feat: Exposes a method for checking is remote track received and played/testing. (#8186)

* feat: Exposes a method for checking is remote track received and played.

Used for some tests in torture.

* squash: Drop not matching string.

Duplicate translation key with not matching content.

* squash: Moves torture specific functions to features/base/testing.

Listens for media events from the video tag of the large video and stores them in redux.

* squash: Fix comments.

* feat: Listens for media events from the video tag of the remote videos and stores them in redux.

* squash: Fix undefined videoTrack if between switches.
This commit is contained in:
Дамян Минков 2020-12-08 08:01:16 -06:00 committed by GitHub
parent 0019284b10
commit 97f47998ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 206 additions and 18 deletions

View File

@ -393,8 +393,7 @@
"toggleFilmstrip": "Show or hide video thumbnails",
"toggleScreensharing": "Switch between camera and screen sharing",
"toggleShortcuts": "Show or hide keyboard shortcuts",
"videoMute": "Start or stop your camera",
"videoQuality": "Manage call quality"
"videoMute": "Start or stop your camera"
},
"liveStreaming": {
"limitNotificationDescriptionWeb": "Due to high demand your streaming will be limited to {{limit}} min. For unlimited streaming try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",

View File

@ -597,16 +597,6 @@ UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
*/
UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
/**
* Returns the video type of the remote participant's video.
* This is needed for the torture clients to determine the video type of the
* remote participants.
*
* @param {string} participantID - The id of the remote participant.
* @returns {string} The video type "camera" or "desktop".
*/
UI.getRemoteVideoType = participantID => VideoLayout.getRemoteVideoType(participantID);
/**
* Sets the remote control active status for a remote participant.
*

View File

@ -12,13 +12,13 @@ import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../../react/features/base/media';
import {
getParticipantById,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import { isRemoteTrackMuted } from '../../../react/features/base/tracks';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
import { PresenceLabel } from '../../../react/features/presence-status';
import {
REMOTE_CONTROL_MENU_STATES,
@ -32,6 +32,15 @@ import SmallVideo from './SmallVideo';
const logger = Logger.getLogger(__filename);
/**
* List of container events that we are going to process, will be added as listener to the
* container for every event in the list. The latest event will be stored in redux.
*/
const containerEvents = [
'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
];
/**
*
* @param {*} spanId
@ -425,6 +434,13 @@ export default class RemoteVideo extends SmallVideo {
// attached we need to update the menu in order to show the volume
// slider.
this.updateRemoteVideoMenu();
} else if (isTestModeEnabled(APP.store.getState())) {
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
containerEvents.forEach(event => {
streamElement.addEventListener(event, cb.bind(this, event));
});
}
}

View File

@ -5,7 +5,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import { ORIENTATION, LargeVideoBackground } from '../../../react/features/large-video';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
@ -19,6 +20,15 @@ export const VIDEO_CONTAINER_TYPE = 'camera';
const FADE_DURATION_MS = 300;
/**
* List of container events that we are going to process, will be added as listener to the
* container for every event in the list. The latest event will be stored in redux.
*/
const containerEvents = [
'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
];
/**
* Returns an array of the video dimensions, so that it keeps it's aspect
* ratio and fits available area with it's larger dimension. This method
@ -259,6 +269,14 @@ export class VideoContainer extends LargeContainer {
this._resizeListeners = new Set();
this.$video[0].onresize = this._onResize.bind(this);
if (isTestModeEnabled(APP.store.getState())) {
const cb = name => APP.store.dispatch(updateLastLargeVideoMediaEvent(name));
containerEvents.forEach(event => {
this.$video[0].addEventListener(event, cb.bind(this, event));
});
}
}
/**

View File

@ -1,5 +1,8 @@
// @flow
import { MEDIA_TYPE } from '../media';
import { getTrackByMediaTypeAndParticipant } from '../tracks';
/**
* Indicates whether the test mode is enabled. When it's enabled
* {@link TestHint} and other components from the testing package will be
@ -13,3 +16,43 @@ export function isTestModeEnabled(state: Object): boolean {
return Boolean(testingConfig && testingConfig.testMode);
}
/**
* Returns the video type of the remote participant's video.
*
* @param {Store} store - The redux store.
* @param {string} id - The participant ID for the remote video.
* @returns {MEDIA_TYPE}
*/
export function getRemoteVideoType({ getState }: Object, id: String): boolean {
return getTrackByMediaTypeAndParticipant(getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, id)?.videoType;
}
/**
* Returns whether the last media event received for large video indicates that the video is playing, if not muted.
*
* @param {Store} store - The redux store.
* @returns {boolean}
*/
export function isLargeVideoReceived({ getState }: Object): boolean {
const largeVideoParticipantId = getState()['features/large-video'].participantId;
const videoTrack = getTrackByMediaTypeAndParticipant(
getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, largeVideoParticipantId);
const lastMediaEvent = getState()['features/large-video'].lastMediaEvent;
return videoTrack && !videoTrack.muted && (lastMediaEvent === 'playing' || lastMediaEvent === 'canplaythrough');
}
/**
* Returns whether the last media event received for a remote video indicates that the video is playing, if not muted.
*
* @param {Store} store - The redux store.
* @param {string} id - The participant ID for the remote video.
* @returns {boolean}
*/
export function isRemoteVideoReceived({ getState }: Object, id: String): boolean {
const videoTrack = getTrackByMediaTypeAndParticipant(getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const lastMediaEvent = videoTrack.lastMediaEvent;
return !videoTrack.muted && (lastMediaEvent === 'playing' || lastMediaEvent === 'canplaythrough');
}

View File

@ -1,10 +1,18 @@
// @flow
import { CONFERENCE_WILL_JOIN } from '../conference';
import { SET_CONFIG } from '../config';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { getJitsiMeetGlobalNS } from '../util';
import { setConnectionState } from './actions';
import {
getRemoteVideoType,
isLargeVideoReceived,
isRemoteVideoReceived,
isTestModeEnabled
} from './functions';
import logger from './logger';
/**
@ -19,6 +27,13 @@ MiddlewareRegistry.register(store => next => action => {
case CONFERENCE_WILL_JOIN:
_bindConferenceConnectionListener(action.conference, store);
break;
case SET_CONFIG: {
const result = next(action);
_bindTortureHelpers(store);
return result;
}
}
return next(action);
@ -52,6 +67,29 @@ function _bindConferenceConnectionListener(conference, { dispatch }) {
null, JitsiConferenceEvents.CONNECTION_INTERRUPTED, dispatch));
}
/**
* Binds all the helper functions needed by torture.
*
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _bindTortureHelpers(store) {
const { getState } = store;
// We bind helpers only if testing mode is enabled
if (!isTestModeEnabled(getState())) {
return;
}
// All torture helper methods go in here
getJitsiMeetGlobalNS().testing = {
getRemoteVideoType: getRemoteVideoType.bind(null, store),
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
};
}
/**
* The handler function for conference connection events which wil store the
* latest even name in the Redux store of feature testing.

View File

@ -104,3 +104,14 @@ export const TRACK_UPDATED = 'TRACK_UPDATED';
* }
*/
export const TRACK_WILL_CREATE = 'TRACK_WILL_CREATE';
/**
* Action to update the redux store with the current media event name of the video track.
*
* @returns {{
* type: TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
* track: Track,
* name: string
* }}
*/
export const TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT = 'TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT';

View File

@ -23,7 +23,8 @@ import {
TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED,
TRACK_UPDATED,
TRACK_WILL_CREATE
TRACK_WILL_CREATE,
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT
} from './actionTypes';
import {
createLocalTracksF,
@ -704,3 +705,22 @@ export function setNoSrcDataNotificationUid(uid) {
uid
};
}
/**
* Updates the last media event received for a video track.
*
* @param {JitsiRemoteTrack} track - JitsiTrack instance.
* @param {string} name - The current media event name for the video.
* @returns {{
* type: TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
* track: Track,
* name: string
* }}
*/
export function updateLastTrackVideoMediaEvent(track, name) {
return {
type: TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
track,
name
};
}

View File

@ -8,6 +8,7 @@ import {
TRACK_CREATE_ERROR,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED,
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
TRACK_UPDATED,
TRACK_WILL_CREATE
} from './actionTypes';
@ -40,6 +41,7 @@ import {
* @param {Track|undefined} state - Track to be modified.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @param {string} action.name - Name of last media event.
* @param {string} action.newValue - New participant ID value (in this
* particular case).
* @param {string} action.oldValue - Old participant ID value (in this
@ -77,6 +79,20 @@ function track(state, action) {
}
break;
}
case TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT: {
const t = action.track;
if (state.jitsiTrack === t) {
if (state.lastMediaEvent !== action.name) {
return {
...state,
lastMediaEvent: action.name
};
}
}
break;
}
case TRACK_NO_DATA_FROM_SOURCE: {
const t = action.track;
@ -104,6 +120,7 @@ ReducerRegistry.register('features/base/tracks', (state = [], action) => {
switch (action.type) {
case PARTICIPANT_ID_CHANGED:
case TRACK_NO_DATA_FROM_SOURCE:
case TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT:
case TRACK_UPDATED:
return state.map(t => track(t, action));

View File

@ -19,3 +19,14 @@ export const SELECT_LARGE_VIDEO_PARTICIPANT
*/
export const UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
= 'UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION';
/**
* Action to update the redux store with the current media event name of large video.
*
* @returns {{
* type: UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT,
* name: string
* }}
*/
export const UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT
= 'UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT';

View File

@ -6,6 +6,8 @@ import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { MEDIA_TYPE } from '../base/media';
import { getTrackByMediaTypeAndParticipant } from '../base/tracks';
import { UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT } from './actionTypes';
export * from './actions.any';
/**
@ -83,3 +85,19 @@ export function resizeLargeVideo(width: number, height: number) {
}
};
}
/**
* Updates the last media event received for the large video.
*
* @param {string} name - The current media event name for the video.
* @returns {{
* type: UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT,
* name: string
* }}
*/
export function updateLastLargeVideoMediaEvent(name: String) {
return {
type: UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT,
name
};
}

View File

@ -5,7 +5,7 @@ import { ReducerRegistry } from '../base/redux';
import {
SELECT_LARGE_VIDEO_PARTICIPANT,
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION, UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT
} from './actionTypes';
ReducerRegistry.register('features/large-video', (state = {}, action) => {
@ -36,6 +36,13 @@ ReducerRegistry.register('features/large-video', (state = {}, action) => {
...state,
resolution: action.resolution
};
case UPDATE_LAST_LARGE_VIDEO_MEDIA_EVENT:
return {
...state,
lastMediaEvent: action.name
};
}
return state;

View File

@ -275,7 +275,7 @@ class Toolbox extends Component<Props, State> {
this._shouldShowButton('videoquality') && {
character: 'A',
exec: this._onShortcutToggleVideoQuality,
helpDescription: 'keyboardShortcuts.videoQuality'
helpDescription: 'toolbar.callQuality'
},
this._shouldShowButton('chat') && {
character: 'C',