[RN] base/media is intent, base/tracks is reality

This commit is contained in:
Lyubo Marinov 2017-08-04 16:06:42 -05:00
parent d600504d85
commit 85a168d51b
11 changed files with 195 additions and 188 deletions

View File

@ -62,13 +62,17 @@ function _addConferenceListeners(conference, dispatch) {
// Dispatches into features/base/media follow:
// FIXME: This is needed because when Jicofo tells us to start muted
// lib-jitsi-meet does the actual muting. Perhaps this should be refactored
// so applications are hinted to start muted, but lib-jitsi-meet doesn't
// take action.
conference.on(
JitsiConferenceEvents.STARTED_MUTED,
() => {
// XXX Jicofo tells lib-jitsi-meet to start with audio and/or video
// muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned
// Jicofo's intent into reality by actually muting the respective
// tracks. The reality is expressed in base/tracks already so what
// is left is to express Jicofo's intent in base/media.
// TODO Maybe the app needs to learn about Jicofo's intent and
// transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet
// acting on Jicofo's intent without the app's knowledge.
dispatch(setAudioMuted(Boolean(conference.startAudioMuted)));
dispatch(setVideoMuted(Boolean(conference.startVideoMuted)));
});

View File

@ -32,7 +32,7 @@ import {
/**
* Implements the middleware of the feature base/conference.
*
* @param {Store} store - Redux store.
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
@ -40,13 +40,13 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_ESTABLISHED:
return _connectionEstablished(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
return _conferenceFailedOrLeft(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case PIN_PARTICIPANT:
return _pinParticipant(store, next, action);
@ -66,13 +66,13 @@ MiddlewareRegistry.register(store => next => action => {
/**
* Notifies the feature base/conference that the action CONNECTION_ESTABLISHED
* is being dispatched within a specific Redux store.
* is being dispatched within a specific redux store.
*
* @param {Store} store - The Redux store in which the specified action is being
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The Redux action CONNECTION_ESTABLISHED which is
* @param {Action} action - The redux action CONNECTION_ESTABLISHED which is
* being dispatched in the specified store.
* @private
* @returns {Object} The new state that is the result of the reduction of the
@ -91,37 +91,37 @@ function _connectionEstablished(store, next, action) {
}
/**
* Does extra sync up on properties that may need to be updated, after
* the conference failed or was left.
* Does extra sync up on properties that may need to be updated after the
* conference failed or was left.
*
* @param {Store} store - The Redux store in which the specified action is being
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The Redux action {@link CONFERENCE_FAILED} or
* @param {Action} action - The redux action {@link CONFERENCE_FAILED} or
* {@link CONFERENCE_LEFT} which is being dispatched in the specified store.
* @private
* @returns {Object} The new state that is the result of the reduction of the
* specified action.
*/
function _conferenceFailedOrLeft(store, next, action) {
function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
const result = next(action);
const { audioOnly } = store.getState()['features/base/conference'];
audioOnly && store.dispatch(setAudioOnly(false));
getState()['features/base/conference'].audioOnly
&& dispatch(setAudioOnly(false));
return result;
}
/**
* Does extra sync up on properties that may need to be updated, after
* the conference was joined.
* Does extra sync up on properties that may need to be updated after the
* conference was joined.
*
* @param {Store} store - The Redux store in which the specified action is being
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The Redux action CONFERENCE_JOINED which is being
* @param {Action} action - The redux action CONFERENCE_JOINED which is being
* dispatched in the specified store.
* @private
* @returns {Object} The new state that is the result of the reduction of the
@ -144,14 +144,14 @@ function _conferenceJoined(store, next, action) {
/**
* Notifies the feature base/conference that the action PIN_PARTICIPANT is being
* dispatched within a specific Redux store. Pins the specified remote
* dispatched within a specific redux store. Pins the specified remote
* participant in the associated conference, ignores the local participant.
*
* @param {Store} store - The Redux store in which the specified action is being
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The Redux action PIN_PARTICIPANT which is being
* @param {Action} action - The redux action PIN_PARTICIPANT which is being
* dispatched in the specified store.
* @private
* @returns {Object} The new state that is the result of the reduction of the
@ -195,26 +195,26 @@ function _pinParticipant(store, next, action) {
* 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.
*
* @param {Store} store - The Redux store in which the specified action is being
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The Redux action SET_AUDIO_ONLY which is being
* @param {Action} action - The redux action SET_AUDIO_ONLY which is being
* dispatched in the specified store.
* @private
* @returns {Object} The new state that is the result of the reduction of the
* specified action.
*/
function _setAudioOnly({ dispatch }, next, action) {
function _setAudioOnly({ dispatch, getState }, next, action) {
const result = next(action);
const { audioOnly } = action;
const { audioOnly } = getState()['features/base/conference'];
// Set lastN to 0 in case audio-only is desired; leave it as undefined,
// otherwise, and the default lastN value will be chosen automatically.
dispatch(setLastN(audioOnly ? 0 : undefined));
// Mute the local video.
// Mute/unmute the local video.
dispatch(setVideoMuted(audioOnly, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
if (typeof APP !== 'undefined') {
@ -229,11 +229,11 @@ function _setAudioOnly({ dispatch }, next, action) {
/**
* Sets the last N (value) of the video channel in the conference.
*
* @param {Store} store - The Redux store in which the specified action is being
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The Redux action SET_LASTN which is being dispatched
* @param {Action} action - The redux action SET_LASTN which is being dispatched
* in the specified store.
* @private
* @returns {Object} The new state that is the result of the reduction of the
@ -257,7 +257,7 @@ function _setLastN(store, next, action) {
* Synchronizes local tracks from state with local tracks in JitsiConference
* instance.
*
* @param {Store} store - Redux store.
* @param {Store} store - The redux store.
* @param {Object} action - Action object.
* @private
* @returns {Promise}
@ -284,13 +284,13 @@ function _syncConferenceLocalTracksWithState(store, action) {
/**
* Notifies the feature base/conference that the action TRACK_ADDED
* or TRACK_REMOVED is being dispatched within a specific Redux store.
* or TRACK_REMOVED is being dispatched within a specific redux store.
*
* @param {Store} store - The Redux store in which the specified action is being
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The Redux action TRACK_ADDED or TRACK_REMOVED which
* @param {Action} action - The redux action TRACK_ADDED or TRACK_REMOVED which
* is being dispatched in the specified store.
* @private
* @returns {Object} The new state that is the result of the reduction of the

View File

@ -72,16 +72,15 @@ function _setRoom({ dispatch, getState }, next, action) {
&& (videoMuted = config.startWithVideoMuted);
// Apply startWithAudioMuted and startWithVideoMuted.
const { audio, video } = state['features/base/media'];
audioMuted = Boolean(audioMuted);
videoMuted = Boolean(videoMuted);
(audio.muted !== audioMuted) && dispatch(setAudioMuted(audioMuted));
(video.facingMode !== CAMERA_FACING_MODE.USER)
&& dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
(Boolean(video.muted) !== videoMuted)
&& dispatch(setVideoMuted(videoMuted));
// Unconditionally express the desires/expectations/intents of the app and
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setVideoMuted(videoMuted));
return next(action);
}

View File

@ -87,17 +87,19 @@ export function destroyLocalTracks() {
*/
export function replaceLocalTrack(oldTrack, newTrack, conference) {
return (dispatch, getState) => {
const currentConference = conference
|| getState()['features/base/conference'].conference;
conference
return currentConference.replaceTrack(oldTrack, newTrack)
// eslint-disable-next-line no-param-reassign
|| (conference = getState()['features/base/conference'].conference);
return conference.replaceTrack(oldTrack, newTrack)
.then(() => {
// We call dispose after doing the replace because
// dispose will try and do a new o/a after the
// track removes itself. Doing it after means
// the JitsiLocalTrack::conference member is already
// cleared, so it won't try and do the o/a
const disposePromise = oldTrack
// We call dispose after doing the replace because dispose will
// try and do a new o/a after the track removes itself. Doing it
// after means the JitsiLocalTrack.conference is already
// cleared, so it won't try and do the o/a.
const disposePromise
= oldTrack
? dispatch(_disposeAndRemoveTracks([ oldTrack ]))
: Promise.resolve();
@ -113,10 +115,12 @@ export function replaceLocalTrack(oldTrack, newTrack, conference) {
// track's mute state. If this is not done, the
// current mute state of the app will be reflected
// on the track, not vice-versa.
const muteAction = newTrack.isVideoTrack()
? setVideoMuted : setAudioMuted;
const setMuted
= newTrack.isVideoTrack()
? setVideoMuted
: setAudioMuted;
return dispatch(muteAction(newTrack.isMuted()));
return dispatch(setMuted(newTrack.isMuted()));
}
})
.then(() => {
@ -366,17 +370,26 @@ export function setTrackMuted(track, muted) {
const f = muted ? 'mute' : 'unmute';
// FIXME: This operation disregards the authority. It is not a problem
// (on mobile) at the moment, but it will be once we start not creating
// tracks early. Refactor this then.
return track[f]().catch(error => {
console.error(`set track ${f} failed`, error);
if (navigator.product === 'ReactNative') {
// Synchronizing the state of base/tracks into the state of
// base/media is not required in React (and, respectively, React
// Native) because base/media expresses the app's and the user's
// desires/expectations/intents and base/tracks expresses
// practice/reality. Unfortunately, the old Web does not comply
// and/or does the opposite.
return;
}
const setMuted
= track.mediaType === MEDIA_TYPE.AUDIO
? setAudioMuted
: setVideoMuted;
// FIXME The following disregards VIDEO_MUTISM_AUTHORITY (in the
// case of setVideoMuted, of course).
dispatch(setMuted(!muted));
});
};

View File

@ -100,35 +100,37 @@ MiddlewareRegistry.register(store => next => action => {
break;
case TRACK_UPDATED:
// TODO Remove the below calls to APP.UI once components interested in
// track mute changes are moved into react.
// TODO Remove the following calls to APP.UI once components interested
// in track mute changes are moved into React and/or redux.
if (typeof APP !== 'undefined') {
const { jitsiTrack } = action.track;
const isMuted = jitsiTrack.isMuted();
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();
const isVideoTrack = jitsiTrack.isVideoTrack();
if (jitsiTrack.isLocal()) {
if (isVideoTrack) {
APP.conference.videoMuted = isMuted;
APP.conference.videoMuted = muted;
} else {
APP.conference.audioMuted = isMuted;
APP.conference.audioMuted = muted;
}
}
if (isVideoTrack) {
APP.UI.setVideoMuted(participantID, isMuted);
APP.UI.setVideoMuted(participantID, muted);
APP.UI.onPeerVideoTypeChanged(
participantID, jitsiTrack.videoType);
participantID,
jitsiTrack.videoType);
} else {
APP.UI.setAudioMuted(participantID, isMuted);
APP.UI.setAudioMuted(participantID, muted);
}
// XXX This function synchronizes track states with media states.
// This is not required in React, because media is the source of
// truth, synchronization should always happen in the media -> track
// direction. The old web, however, does the opposite, hence the
// need for this.
// XXX The following synchronizes the state of base/tracks into the
// state of base/media. Which is not required in React (and,
// respectively, React Native) because base/media expresses the
// app's and the user's desires/expectations/intents and base/tracks
// expresses practice/reality. Unfortunately, the old Web does not
// comply and/or does the opposite. Hence, the following:
return _trackUpdated(store, next, action);
}
@ -149,8 +151,8 @@ MiddlewareRegistry.register(store => next => action => {
* @returns {Track} The local <tt>Track</tt> associated with the specified
* <tt>mediaType</tt> in the specified <tt>store</tt>.
*/
function _getLocalTrack(store, mediaType: MEDIA_TYPE) {
return getLocalTrack(store.getState()['features/base/tracks'], mediaType);
function _getLocalTrack({ getState }, mediaType: MEDIA_TYPE) {
return getLocalTrack(getState()['features/base/tracks'], mediaType);
}
/**

View File

@ -16,10 +16,10 @@ import { MiddlewareRegistry } from '../../base/redux';
* based on the type of conference. Audio-only conferences don't use the speaker
* by default, and video conferences do.
*
* @param {Store} store - Redux store.
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
MiddlewareRegistry.register(({ getState }) => next => action => {
const AudioMode = NativeModules.AudioMode;
if (AudioMode) {
@ -32,34 +32,20 @@ MiddlewareRegistry.register(store => next => action => {
mode = AudioMode.DEFAULT;
break;
case CONFERENCE_WILL_JOIN: {
const { audioOnly } = store.getState()['features/base/conference'];
mode = audioOnly ? AudioMode.AUDIO_CALL : AudioMode.VIDEO_CALL;
break;
}
case CONFERENCE_WILL_JOIN:
case SET_AUDIO_ONLY: {
const { conference } = store.getState()['features/base/conference'];
if (conference) {
if (getState()['features/base/conference'].conference
|| action.conference) {
mode
= action.audioOnly
? AudioMode.AUDIO_CALL
: AudioMode.VIDEO_CALL;
} else {
mode = null;
}
break;
}
default:
mode = null;
break;
}
if (mode !== null) {
if (typeof mode !== 'undefined') {
AudioMode.setMode(mode)
.catch(err =>
console.error(

View File

@ -1,10 +1,7 @@
import { setLastN } from '../../base/conference';
import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media';
import {
_SET_APP_STATE_LISTENER,
APP_STATE_CHANGED
} from './actionTypes';
import { _SET_APP_STATE_LISTENER, APP_STATE_CHANGED } from './actionTypes';
/**
* Sets the listener to be used with React Native's AppState API.
@ -41,7 +38,7 @@ export function _setBackgroundVideoMuted(muted: boolean) {
// for last N will be chosen automatically.
const { audioOnly } = getState()['features/base/conference'];
!audioOnly && dispatch(setLastN(muted ? 0 : undefined));
audioOnly || dispatch(setLastN(muted ? 0 : undefined));
dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
};
}

View File

@ -22,29 +22,31 @@ import { MiddlewareRegistry } from '../../base/redux';
* In immersive mode the status and navigation bars are hidden and thus the
* entire screen will be covered by our application.
*
* @param {Store} store - Redux store.
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
MiddlewareRegistry.register(({ getState }) => next => action => {
const result = next(action);
let fullScreen = null;
switch (action.type) {
case APP_STATE_CHANGED: {
// Check if we just came back from the background and reenable full
case APP_STATE_CHANGED:
case CONFERENCE_WILL_JOIN:
case HIDE_DIALOG:
case SET_AUDIO_ONLY: {
// Check if we just came back from the background and re-enable full
// screen mode if necessary.
if (action.appState === 'active') {
const { audioOnly, conference }
= store.getState()['features/base/conference'];
const { appState } = action;
fullScreen = conference ? !audioOnly : false;
}
if (typeof appState !== 'undefined' && appState !== 'active') {
break;
}
case CONFERENCE_WILL_JOIN: {
const { audioOnly } = store.getState()['features/base/conference'];
const { audioOnly, conference }
= getState()['features/base/conference'];
fullScreen = !audioOnly;
fullScreen = conference || action.conference ? !audioOnly : false;
break;
}
@ -52,21 +54,6 @@ MiddlewareRegistry.register(store => next => action => {
case CONFERENCE_LEFT:
fullScreen = false;
break;
case HIDE_DIALOG: {
const { audioOnly, conference }
= store.getState()['features/base/conference'];
fullScreen = conference ? !audioOnly : false;
break;
}
case SET_AUDIO_ONLY: {
const { conference } = store.getState()['features/base/conference'];
fullScreen = conference ? !action.audioOnly : false;
break;
}
}
if (fullScreen !== null) {
@ -75,7 +62,7 @@ MiddlewareRegistry.register(store => next => action => {
console.warn(`Failed to set full screen mode: ${err}`));
}
return next(action);
return result;
});
/**

View File

@ -14,32 +14,29 @@ import { MiddlewareRegistry } from '../../base/redux';
* the screen and disable touch controls when an object is nearby. The
* functionality is enabled when a conference is in audio-only mode.
*
* @param {Store} store - Redux store.
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
MiddlewareRegistry.register(({ getState }) => next => action => {
const result = next(action);
switch (action.type) {
case CONFERENCE_JOINED: {
const { audioOnly } = store.getState()['features/base/conference'];
_setProximityEnabled(audioOnly);
break;
}
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
_setProximityEnabled(false);
break;
case CONFERENCE_JOINED:
case SET_AUDIO_ONLY: {
const { conference } = store.getState()['features/base/conference'];
const { audioOnly, conference }
= getState()['features/base/conference'];
conference && _setProximityEnabled(action.audioOnly);
conference && _setProximityEnabled(audioOnly);
break;
}
}
return next(action);
return result;
});
/**

View File

@ -3,7 +3,12 @@ import { View } from 'react-native';
import { connect } from 'react-redux';
import { toggleAudioOnly } from '../../base/conference';
import { MEDIA_TYPE, toggleCameraFacingMode } from '../../base/media';
import {
MEDIA_TYPE,
setAudioMuted,
setVideoMuted,
toggleCameraFacingMode
} from '../../base/media';
import { Container } from '../../base/react';
import { ColorPalette } from '../../base/styles';
import { beginRoomLockRequest } from '../../room-lock';
@ -56,11 +61,6 @@ class Toolbox extends Component {
*/
_onShareRoom: React.PropTypes.func,
/**
* Handler for toggle audio.
*/
_onToggleAudio: React.PropTypes.func,
/**
* Toggles the audio-only flag of the conference.
*/
@ -72,11 +72,6 @@ class Toolbox extends Component {
*/
_onToggleCameraFacingMode: React.PropTypes.func,
/**
* Handler for toggling video.
*/
_onToggleVideo: React.PropTypes.func,
/**
* Flag showing whether video is muted.
*/
@ -85,9 +80,25 @@ class Toolbox extends Component {
/**
* Flag showing whether toolbar is visible.
*/
_visible: React.PropTypes.bool
_visible: React.PropTypes.bool,
dispatch: React.PropTypes.func
};
/**
* Initializes a new {@code Toolbox} instance.
*
* @param {Object} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onToggleAudio = this._onToggleAudio.bind(this);
this._onToggleVideo = this._onToggleVideo.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
@ -144,6 +155,36 @@ class Toolbox extends Component {
};
}
/**
* Dispatches an action to toggle the mute state of the audio/microphone.
*
* @private
* @returns {void}
*/
_onToggleAudio() {
// The user sees the reality i.e. the state of base/tracks and intends
// to change reality by tapping on the respective button i.e. the user
// sets the state of base/media. Whether the user's intention will turn
// into reality is a whole different story which is of no concern to the
// tapping.
this.props.dispatch(setAudioMuted(!this.props._audioMuted));
}
/**
* Dispatches an action to toggle the mute state of the video/camera.
*
* @private
* @returns {void}
*/
_onToggleVideo() {
// The user sees the reality i.e. the state of base/tracks and intends
// to change reality by tapping on the respective button i.e. the user
// sets the state of base/media. Whether the user's intention will turn
// into reality is a whole different story which is of no concern to the
// tapping.
this.props.dispatch(setVideoMuted(!this.props._videoMuted));
}
/**
* Renders the toolbar which contains the primary buttons such as hangup,
* audio and video mute.
@ -162,7 +203,7 @@ class Toolbox extends Component {
<ToolbarButton
iconName = { audioButtonStyles.iconName }
iconStyle = { audioButtonStyles.iconStyle }
onClick = { this.props._onToggleAudio }
onClick = { this._onToggleAudio }
style = { audioButtonStyles.style } />
<ToolbarButton
iconName = 'hangup'
@ -174,7 +215,7 @@ class Toolbox extends Component {
disabled = { this.props._audioOnly }
iconName = { videoButtonStyles.iconName }
iconStyle = { videoButtonStyles.iconStyle }
onClick = { this.props._onToggleVideo }
onClick = { this._onToggleVideo }
style = { videoButtonStyles.style } />
</View>
);

View File

@ -3,7 +3,6 @@
import type { Dispatch } from 'redux';
import { appNavigate } from '../app';
import { toggleAudioMuted, toggleVideoMuted } from '../base/media';
import { getLocalAudioTrack, getLocalVideoTrack } from '../base/tracks';
/**
@ -19,6 +18,11 @@ import { getLocalAudioTrack, getLocalVideoTrack } from '../base/tracks';
*/
export function abstractMapDispatchToProps(dispatch: Dispatch<*>): Object {
return {
// Inject {@code dispatch} into the React Component's props in case it
// needs to dispatch an action in the redux store without
// {@code mapDispatchToProps}.
dispatch,
/**
* Dispatches action to leave the current conference.
*
@ -33,29 +37,6 @@ export function abstractMapDispatchToProps(dispatch: Dispatch<*>): Object {
// expression of (1) the lack of knowledge & (2) the desire to no
// longer have a valid room name to join.
dispatch(appNavigate(undefined));
},
/**
* Dispatches an action to toggle the mute state of the
* audio/microphone.
*
* @private
* @returns {Object} - Dispatched action.
* @type {Function}
*/
_onToggleAudio() {
dispatch(toggleAudioMuted());
},
/**
* Dispatches an action to toggle the mute state of the video/camera.
*
* @private
* @returns {Object} - Dispatched action.
* @type {Function}
*/
_onToggleVideo() {
dispatch(toggleVideoMuted());
}
};
}