feat(multi-stream-support) Replace participant connection status logic with track streaming status (#10934)

This commit is contained in:
William Liang 2022-02-23 08:30:10 -05:00 committed by GitHub
parent fa65a54f50
commit 05dc018671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 345 additions and 82 deletions

View File

@ -9,10 +9,8 @@ import { Provider } from 'react-redux';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
import { Avatar } from '../../../react/features/base/avatar';
import theme from '../../../react/features/base/components/themes/participantsPaneTheme.json';
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import {
getParticipantById,
@ -20,6 +18,14 @@ import {
} from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
isParticipantConnectionStatusActive,
isParticipantConnectionStatusInactive,
isParticipantConnectionStatusInterrupted,
isTrackStreamingStatusActive,
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../../react/features/connection-indicator/functions';
import {
updateKnownLargeVideoResolution
} from '../../../react/features/large-video/actions';
@ -226,8 +232,20 @@ export default class LargeVideoManager {
const state = APP.store.getState();
const participant = getParticipantById(state, id);
const connectionStatus = participant?.connectionStatus;
const isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || connectionStatus === JitsiParticipantConnectionStatus.ACTIVE);
let isVideoRenderable;
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || isTrackStreamingStatusActive(videoTrack));
} else {
isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || isParticipantConnectionStatusActive(participant));
}
const isAudioOnly = APP.conference.isAudioOnly();
const showAvatar
= isVideoContainer
@ -278,8 +296,16 @@ export default class LargeVideoManager {
this.updateLargeVideoAudioLevel(0);
}
const messageKey
= connectionStatus === JitsiParticipantConnectionStatus.INACTIVE ? 'connection.LOW_BANDWIDTH' : null;
let messageKey;
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
messageKey = isTrackStreamingStatusInactive(videoTrack) ? 'connection.LOW_BANDWIDTH' : null;
} else {
messageKey = isParticipantConnectionStatusInactive(participant) ? 'connection.LOW_BANDWIDTH' : null;
}
// Do not show connection status message in the audio only mode,
// because it's based on the video playback status.
@ -505,13 +531,22 @@ export default class LargeVideoManager {
showRemoteConnectionMessage(show) {
if (typeof show !== 'boolean') {
const participant = getParticipantById(APP.store.getState(), this.id);
const connStatus = participant?.connectionStatus;
const state = APP.store.getState();
// eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id)
&& (connStatus === JitsiParticipantConnectionStatus.INTERRUPTED
|| connStatus
=== JitsiParticipantConnectionStatus.INACTIVE);
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id);
// eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id)
&& (isTrackStreamingStatusInterrupted(videoTrack)
|| isTrackStreamingStatusInactive(videoTrack));
} else {
// eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id)
&& (isParticipantConnectionStatusInterrupted(participant)
|| isParticipantConnectionStatusInactive(participant));
}
}
if (show) {

View File

@ -19,6 +19,7 @@ export const JitsiE2ePingEvents = JitsiMeetJS.events.e2eping;
export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
export const JitsiParticipantConnectionStatus
= JitsiMeetJS.constants.participantConnectionStatus;
export const JitsiTrackStreamingStatus = JitsiMeetJS.constants.trackStreamingStatus;
export const JitsiRecordingConstants = JitsiMeetJS.constants.recording;
export const JitsiSIPVideoGWStatus = JitsiMeetJS.constants.sipVideoGW;
export const JitsiTrackErrors = JitsiMeetJS.errors.track;

View File

@ -553,6 +553,26 @@ export function trackVideoTypeChanged(track, videoType) {
};
}
/**
* Create an action for when track streaming status changes.
*
* @param {(JitsiRemoteTrack)} track - JitsiTrack instance.
* @param {string} streamingStatus - The new streaming status of the track.
* @returns {{
* type: TRACK_UPDATED,
* track: Track
* }}
*/
export function trackStreamingStatusChanged(track, streamingStatus) {
return {
type: TRACK_UPDATED,
track: {
jitsiTrack: track,
streamingStatus
}
};
}
/**
* Signals passed tracks to be added.
*

View File

@ -5,12 +5,19 @@ import clsx from 'clsx';
import React from 'react';
import type { Dispatch } from 'redux';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { translate } from '../../../base/i18n';
import { Icon, IconConnectionActive, IconConnectionInactive } from '../../../base/icons';
import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import {
isParticipantConnectionStatusInactive,
isParticipantConnectionStatusInterrupted,
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../functions';
import AbstractConnectionIndicator, {
INDICATOR_DISPLAY_THRESHOLD,
type Props as AbstractProps,
@ -18,6 +25,7 @@ import AbstractConnectionIndicator, {
} from '../AbstractConnectionIndicator';
import ConnectionIndicatorContent from './ConnectionIndicatorContent';
import { ConnectionIndicatorIcon } from './ConnectionIndicatorIcon';
/**
* An array of display configurations for the connection indicator and its bars.
@ -237,17 +245,22 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {string}
*/
_getConnectionColorClass() {
const { _connectionStatus } = this.props;
// TODO We currently do not have logic to emit and handle stats changes for tracks.
const { percent } = this.state.stats;
const { INACTIVE, INTERRUPTED } = JitsiParticipantConnectionStatus;
if (_connectionStatus === INACTIVE) {
if (this.props._connectionIndicatorInactiveDisabled) {
const {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_connectionIndicatorInactiveDisabled
} = this.props;
if (_isConnectionStatusInactive) {
if (_connectionIndicatorInactiveDisabled) {
return 'status-disabled';
}
return 'status-other';
} else if (_connectionStatus === INTERRUPTED) {
} else if (_isConnectionStatusInterrupted) {
return 'status-lost';
} else if (typeof percent === 'undefined') {
return 'status-high';
@ -279,12 +292,12 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {string}
*/
_getVisibilityClass() {
const { _connectionStatus, classes } = this.props;
const { _isConnectionStatusInactive, _isConnectionStatusInterrupted, classes } = this.props;
return this.state.showIndicator
|| this.props.alwaysVisible
|| _connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED
|| _connectionStatus === JitsiParticipantConnectionStatus.INACTIVE
|| _isConnectionStatusInterrupted
|| _isConnectionStatusInactive
? '' : classes.hidden;
}
@ -300,49 +313,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
this.setState({ popoverVisible: false });
}
/**
* Creates a ReactElement for displaying an icon that represents the current
* connection quality.
*
* @returns {ReactElement}
*/
_renderIcon() {
const colorClass = this._getConnectionColorClass();
if (this.props._connectionStatus === JitsiParticipantConnectionStatus.INACTIVE) {
if (this.props._connectionIndicatorInactiveDisabled) {
return null;
}
return (
<span className = 'connection_ninja'>
<Icon
className = { clsx(this.props.classes.icon, this.props.classes.inactiveIcon, colorClass) }
size = { 24 }
src = { IconConnectionInactive } />
</span>
);
}
let emptyIconWrapperClassName = 'connection_empty';
if (this.props._connectionStatus
=== JitsiParticipantConnectionStatus.INTERRUPTED) {
// emptyIconWrapperClassName is used by the torture tests to
// identify lost connection status handling.
emptyIconWrapperClassName = 'connection_lost';
}
return (
<span className = { emptyIconWrapperClassName }>
<Icon
className = { clsx(this.props.classes.icon, colorClass) }
size = { 12 }
src = { IconConnectionActive } />
</span>
);
}
_onShowPopover: () => void;
@ -363,10 +333,25 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {ReactElement}
*/
_renderIndicator() {
const {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_connectionIndicatorInactiveDisabled,
_videoTrack,
classes,
iconSize
} = this.props;
return (
<div
style = {{ fontSize: this.props.iconSize }}>
{this._renderIcon()}
style = {{ fontSize: iconSize }}>
<ConnectionIndicatorIcon
classes = { classes }
colorClass = { this._getConnectionColorClass() }
connectionIndicatorInactiveDisabled = { _connectionIndicatorInactiveDisabled }
isConnectionStatusInactive = { _isConnectionStatusInactive }
isConnectionStatusInterrupted = { _isConnectionStatusInterrupted }
track = { _videoTrack } />
</div>
);
}
@ -381,14 +366,27 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { participantId } = ownProps;
const participant
= participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
const participant = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
const _isConnectionStatusInactive = sourceNameSignalingEnabled
? isTrackStreamingStatusInactive(firstVideoTrack)
: isParticipantConnectionStatusInactive(participant);
const _isConnectionStatusInterrupted = sourceNameSignalingEnabled
? isTrackStreamingStatusInterrupted(firstVideoTrack)
: isParticipantConnectionStatusInterrupted(participant);
return {
_connectionIndicatorInactiveDisabled:
Boolean(state['features/base/config'].connectionIndicators?.inactiveDisabled),
_popoverDisabled: state['features/base/config'].connectionIndicators?.disableDetails,
_connectionStatus: participant?.connectionStatus
_videoTrack: firstVideoTrack,
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
};
}
export default translate(connect(_mapStateToProps)(

View File

@ -3,14 +3,20 @@
import React from 'react';
import type { Dispatch } from 'redux';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { translate } from '../../../base/i18n';
import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { ConnectionStatsTable } from '../../../connection-stats';
import { saveLogs } from '../../actions';
import {
isParticipantConnectionStatusInactive,
isParticipantConnectionStatusInterrupted,
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../functions';
import AbstractConnectionIndicator, {
INDICATOR_DISPLAY_THRESHOLD,
type Props as AbstractProps,
@ -217,12 +223,14 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
_getConnectionStatusTip() {
let tipKey;
switch (this.props._connectionStatus) {
case JitsiParticipantConnectionStatus.INTERRUPTED:
const { _isConnectionStatusInactive, _isConnectionStatusInterrupted } = this.props;
switch (true) {
case _isConnectionStatusInterrupted:
tipKey = 'connectionindicator.quality.lost';
break;
case JitsiParticipantConnectionStatus.INACTIVE:
case _isConnectionStatusInactive:
tipKey = 'connectionindicator.quality.inactive';
break;
@ -310,17 +318,29 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
const conference = state['features/base/conference'].conference;
const participant
= participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const _isConnectionStatusInactive = sourceNameSignalingEnabled
? isTrackStreamingStatusInactive(firstVideoTrack)
: isParticipantConnectionStatusInactive(participant);
const _isConnectionStatusInterrupted = sourceNameSignalingEnabled
? isTrackStreamingStatusInterrupted(firstVideoTrack)
: isParticipantConnectionStatusInterrupted(participant);
const props = {
_connectionStatus: participant?.connectionStatus,
_enableSaveLogs: state['features/base/config'].enableSaveLogs,
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
_isLocalVideo: participant?.local,
_region: participant?.region
_region: participant?.region,
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
};
if (conference) {
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
const firstAudioTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantId);

View File

@ -0,0 +1,110 @@
// @flow
import clsx from 'clsx';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { Icon, IconConnectionActive, IconConnectionInactive } from '../../../base/icons';
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
import { trackStreamingStatusChanged } from '../../../base/tracks';
type Props = {
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* A CSS class that interprets the current connection status as a color.
*/
colorClass: string,
/**
* Disable/enable inactive indicator.
*/
connectionIndicatorInactiveDisabled: boolean,
/**
* JitsiTrack instance.
*/
track: Object,
/**
* Whether or not the connection status is inactive.
*/
isConnectionStatusInactive: boolean,
/**
* Whether or not the connection status is interrupted.
*/
isConnectionStatusInterrupted: boolean,
}
export const ConnectionIndicatorIcon = ({
classes,
colorClass,
connectionIndicatorInactiveDisabled,
isConnectionStatusInactive,
isConnectionStatusInterrupted,
track
}: Props) => {
const sourceNameSignalingEnabled = useSelector(state => getSourceNameSignalingFeatureFlag(state));
const dispatch = useDispatch();
const sourceName = track?.jitsiTrack?.getSourceName?.();
const handleTrackStreamingStatusChanged = streamingStatus => {
dispatch(trackStreamingStatusChanged(track.jitsiTrack, streamingStatus));
};
useEffect(() => {
if (track && !track.local && sourceNameSignalingEnabled) {
track.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED, handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(track.jitsiTrack, track.jitsiTrack.getTrackStreamingStatus?.()));
}
return () => {
if (track && !track.local && sourceNameSignalingEnabled) {
track.jitsiTrack.off(
JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
handleTrackStreamingStatusChanged
);
dispatch(trackStreamingStatusChanged(track.jitsiTrack, track.jitsiTrack.getTrackStreamingStatus?.()));
}
};
}, [ sourceName ]);
if (isConnectionStatusInactive) {
if (connectionIndicatorInactiveDisabled) {
return null;
}
return (
<span className = 'connection_ninja'>
<Icon
className = { clsx(classes.icon, classes.inactiveIcon, colorClass) }
size = { 24 }
src = { IconConnectionInactive } />
</span>
);
}
let emptyIconWrapperClassName = 'connection_empty';
if (isConnectionStatusInterrupted) {
// emptyIconWrapperClassName is used by the torture tests to identify lost connection status handling.
emptyIconWrapperClassName = 'connection_lost';
}
return (
<span className = { emptyIconWrapperClassName }>
<Icon
className = { clsx(classes.icon, colorClass) }
size = { 12 }
src = { IconConnectionActive } />
</span>
);
};

View File

@ -0,0 +1,73 @@
import { JitsiParticipantConnectionStatus, JitsiTrackStreamingStatus } from '../base/lib-jitsi-meet';
/**
* Checks if the passed track's streaming status is active.
*
* @param {Object} videoTrack - Track reference.
* @returns {boolean} - Is streaming status active.
*/
export function isTrackStreamingStatusActive(videoTrack) {
const streamingStatus = videoTrack?.streamingStatus;
return streamingStatus === JitsiTrackStreamingStatus.ACTIVE;
}
/**
* Checks if the passed track's streaming status is inactive.
*
* @param {Object} videoTrack - Track reference.
* @returns {boolean} - Is streaming status inactive.
*/
export function isTrackStreamingStatusInactive(videoTrack) {
const streamingStatus = videoTrack?.streamingStatus;
return streamingStatus === JitsiTrackStreamingStatus.INACTIVE;
}
/**
* Checks if the passed track's streaming status is interrupted.
*
* @param {Object} videoTrack - Track reference.
* @returns {boolean} - Is streaming status interrupted.
*/
export function isTrackStreamingStatusInterrupted(videoTrack) {
const streamingStatus = videoTrack?.streamingStatus;
return streamingStatus === JitsiTrackStreamingStatus.INTERRUPTED;
}
/**
* Checks if the passed participant's connecton status is active.
*
* @param {Object} participant - Participant reference.
* @returns {boolean} - Is connection status active.
*/
export function isParticipantConnectionStatusActive(participant) {
const connectionStatus = participant?.connectionStatus;
return connectionStatus === JitsiParticipantConnectionStatus.ACTIVE;
}
/**
* Checks if the passed participant's connecton status is inactive.
*
* @param {Object} participant - Participant reference.
* @returns {boolean} - Is connection status inactive.
*/
export function isParticipantConnectionStatusInactive(participant) {
const connectionStatus = participant?.connectionStatus;
return connectionStatus === JitsiParticipantConnectionStatus.INACTIVE;
}
/**
* Checks if the passed participant's connecton status is interrupted.
*
* @param {Object} participant - Participant reference.
* @returns {boolean} - Is connection status interrupted.
*/
export function isParticipantConnectionStatusInterrupted(participant) {
const connectionStatus = participant?.connectionStatus;
return connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED;
}

View File

@ -1,6 +1,6 @@
// @flow
import { JitsiParticipantConnectionStatus } from '../base/lib-jitsi-meet';
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { MEDIA_TYPE } from '../base/media';
import {
getLocalParticipant,
@ -15,6 +15,7 @@ import {
isLocalTrackMuted,
isRemoteTrackMuted
} from '../base/tracks/functions';
import { isTrackStreamingStatusActive, isParticipantConnectionStatusActive } from '../connection-indicator/functions';
import { LAYOUTS } from '../video-layout';
import {
@ -105,7 +106,7 @@ export function isVideoPlayable(stateful: Object | Function, id: String) {
const tracks = state['features/base/tracks'];
const participant = id ? getParticipantById(state, id) : getLocalParticipant(state);
const isLocal = participant?.local ?? true;
const { connectionStatus } = participant || {};
const videoTrack
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
const isAudioOnly = Boolean(state['features/base/audio-only'].enabled);
@ -118,8 +119,13 @@ export function isVideoPlayable(stateful: Object | Function, id: String) {
} else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
const isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, id);
isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
&& connectionStatus === JitsiParticipantConnectionStatus.ACTIVE;
if (getSourceNameSignalingFeatureFlag(state)) {
isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
&& isTrackStreamingStatusActive(videoTrack);
} else {
isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
&& isParticipantConnectionStatusActive(participant);
}
}
return isPlayable;