From 97f47998ba1dd8dc0dad4860f7f173fcdd278f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Tue, 8 Dec 2020 08:01:16 -0600 Subject: [PATCH] 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. --- lang/main.json | 3 +- modules/UI/UI.js | 10 ----- modules/UI/videolayout/RemoteVideo.js | 20 ++++++++- modules/UI/videolayout/VideoContainer.js | 20 ++++++++- react/features/base/testing/functions.js | 43 +++++++++++++++++++ react/features/base/testing/middleware.js | 38 ++++++++++++++++ react/features/base/tracks/actionTypes.js | 11 +++++ react/features/base/tracks/actions.js | 22 +++++++++- react/features/base/tracks/reducer.js | 17 ++++++++ react/features/large-video/actionTypes.js | 11 +++++ react/features/large-video/actions.web.js | 18 ++++++++ react/features/large-video/reducer.js | 9 +++- .../toolbox/components/web/Toolbox.js | 2 +- 13 files changed, 206 insertions(+), 18 deletions(-) diff --git a/lang/main.json b/lang/main.json index da1831154..2862a9876 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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 {{app}}.", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index fa4021287..94f7bb80a 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -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. * diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index b5ec0b621..5e20316cf 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -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)); + }); } } diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index 445416604..2b553ee46 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -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)); + }); + } } /** diff --git a/react/features/base/testing/functions.js b/react/features/base/testing/functions.js index 2ee2965e3..c2d22ba8f 100644 --- a/react/features/base/testing/functions.js +++ b/react/features/base/testing/functions.js @@ -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'); +} diff --git a/react/features/base/testing/middleware.js b/react/features/base/testing/middleware.js index 7e0e76d28..1ffa82a61 100644 --- a/react/features/base/testing/middleware.js +++ b/react/features/base/testing/middleware.js @@ -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. diff --git a/react/features/base/tracks/actionTypes.js b/react/features/base/tracks/actionTypes.js index e59a8cdb9..5cf66a6f9 100644 --- a/react/features/base/tracks/actionTypes.js +++ b/react/features/base/tracks/actionTypes.js @@ -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'; diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index 328eb16c6..bee9a0bc6 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -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 + }; +} diff --git a/react/features/base/tracks/reducer.js b/react/features/base/tracks/reducer.js index ec678f322..35dcf6d38 100644 --- a/react/features/base/tracks/reducer.js +++ b/react/features/base/tracks/reducer.js @@ -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)); diff --git a/react/features/large-video/actionTypes.js b/react/features/large-video/actionTypes.js index 77436be7b..4f2ee2b95 100644 --- a/react/features/large-video/actionTypes.js +++ b/react/features/large-video/actionTypes.js @@ -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'; diff --git a/react/features/large-video/actions.web.js b/react/features/large-video/actions.web.js index d19e06836..4fce094bd 100644 --- a/react/features/large-video/actions.web.js +++ b/react/features/large-video/actions.web.js @@ -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 + }; +} diff --git a/react/features/large-video/reducer.js b/react/features/large-video/reducer.js index ebee066d1..a460592ea 100644 --- a/react/features/large-video/reducer.js +++ b/react/features/large-video/reducer.js @@ -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; diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index 9906c0f68..f8ec7f1f2 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -275,7 +275,7 @@ class Toolbox extends Component { this._shouldShowButton('videoquality') && { character: 'A', exec: this._onShortcutToggleVideoQuality, - helpDescription: 'keyboardShortcuts.videoQuality' + helpDescription: 'toolbar.callQuality' }, this._shouldShowButton('chat') && { character: 'C',