Compare commits

...

2 Commits

Author SHA1 Message Date
yanas 7b60187847 feat(transcription-label):Adding a transcription status label 2017-09-19 13:50:45 -05:00
yanas 62a8ceecce feat(recording):Recording popup menu 2017-09-19 13:41:04 -05:00
24 changed files with 658 additions and 72 deletions

View File

@ -152,7 +152,7 @@ var config = { // eslint-disable-line no-unused-vars
// Whether to enable recording or not.
//enableRecording: false,
// Type for recording: one of jibri or jirecon.
// Type for recording: one of jibri, jibri_file or jirecon.
//recordingType: 'jibri',
// Misc

View File

@ -26,6 +26,13 @@ p {
margin: 0;
}
h4 {
font-size: 14px;
font-weight: 600;
letter-spacing: -0.006em;
line-height: 16px;
}
body, input, textarea, keygen, select, button {
font-family: $baseFontFamily !important;
}

View File

@ -1,3 +1,41 @@
/**
* The recording spinner style.
*/
.recordingSpinner {
vertical-align: top;
}
/**
* The recording button popup style.
*/
.recording-popup-dialog {
width: 140px;
/**
* The popup item style.
*/
.recording-popup-item {
font-size: 12px;
position: relative;
top: -5px;
}
/**
* The popup title style.
*/
.recording-popup-title {
margin-bottom: 10px;
}
}
/**
* The transcription label style.
*/
#transcriptionLabel {
border-radius: 3px;
> span {
line-height: 40px;
padding-left: 12px;
}
}

View File

@ -162,7 +162,7 @@
transition: right 0.5s;
&.with-filmstrip.with-remote-videos {
&#recordingLabel {
&#transcriptionLabel {
right: 200px;
}

View File

@ -95,8 +95,8 @@
}
.connection-indicator,
div.indicator-container,
{
div.indicator-container
{
margin-right: 4px;
}

View File

@ -127,6 +127,7 @@
border-radius: 50%;
position: absolute;
box-sizing: border-box;
z-index: #{$tooltipsZ + 1};
i {
cursor: pointer;
@ -145,13 +146,9 @@
z-index: $tooltipsZ;
}
#videoResolutionLabel {
z-index: #{$tooltipsZ + 1};
}
.centeredVideoLabel {
bottom: 45%;
border-radius: 2px;
border-radius: 3px;
display: none;
padding: 10px;
transform: translate(-50%, 0);

View File

@ -35,7 +35,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
//main toolbar
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup', // jshint ignore:line
//extended toolbar
'profile', 'addtocall', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'videoquality', 'filmstrip'], // jshint ignore:line
'profile', 'addtocall', 'contacts', 'chat', 'recording', 'transcription', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'videoquality', 'filmstrip'], // jshint ignore:line
/**
* Main Toolbar Buttons
* All of them should be in TOOLBAR_BUTTONS

View File

@ -401,7 +401,10 @@
"failedToStart": "Recording failed to start",
"buttonTooltip": "Start / Stop recording",
"error": "Recording failed. Please try again.",
"unavailable": "The recording service is currently unavailable. Please try again later."
"unavailable": "The recording service is currently unavailable. Please try again later.",
"recordingPopupTitle": "Recording options",
"videoRecordingLabel": "Video",
"transcriptionLabel": "Transcription"
},
"liveStreaming":
{

View File

@ -477,7 +477,6 @@ UI.updateLocalRole = isModerator => {
APP.store.dispatch(showDialOutButton(isModerator));
APP.store.dispatch(showSharedVideoButton());
Recording.showRecordingButton(isModerator);
SettingsMenu.showStartMutedOptions(isModerator);
SettingsMenu.showFollowMeOptions(isModerator);

View File

@ -1,4 +1,4 @@
/* global APP, $, config, interfaceConfig, JitsiMeetJS */
/* global APP, config, interfaceConfig, JitsiMeetJS */
/*
* Copyright @ 2015 Atlassian Pty Ltd
*
@ -249,8 +249,7 @@ function isStartingStatus(status) {
/**
* Manages the recording user interface and user experience.
* @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
* updateRecordingUI, checkAutoRecord}}
* @type {{init, updateRecordingState, updateRecordingUI, checkAutoRecord}}
*/
var Recording = {
/**
@ -271,13 +270,6 @@ var Recording = {
Object.assign(this, RECORDING_TRANSLATION_KEYS);
}
// XXX Due to the React-ification of Toolbox, the HTMLElement with id
// toolbar_button_record may not exist yet.
$(document).on(
'click',
'#toolbar_button_record',
ev => this._onToolbarButtonClick(ev));
// If I am a recorder then I publish my recorder custom role to notify
// everyone.
if (config.iAmRecorder) {
@ -290,28 +282,6 @@ var Recording = {
}
},
/**
* Initialise the recording button.
*/
initRecordingButton() {
const selector = $('#toolbar_button_record');
selector.addClass(this.baseClass);
selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip);
APP.translation.translateElement(selector);
},
/**
* Shows or hides the 'recording' button.
* @param show {true} to show the recording button, {false} to hide it
*/
showRecordingButton(show) {
let shouldShow = show && _isRecordingButtonEnabled();
let id = 'toolbar_button_record';
UIUtil.setVisible(id, shouldShow);
},
/**
* Updates the recording state UI.
* @param recordingState gives us the current recording state
@ -338,6 +308,7 @@ var Recording = {
this.currentState = recordingState;
let labelDisplayConfiguration;
let recordingButtonToggled;
switch (recordingState) {
case Status.ON:
@ -348,7 +319,7 @@ var Recording = {
showSpinner: recordingState === Status.RETRYING
};
this._setToolbarButtonToggled(true);
recordingButtonToggled = true;
break;
}
@ -372,7 +343,7 @@ var Recording = {
: this.recordingOffKey
};
this._setToolbarButtonToggled(false);
recordingButtonToggled = false;
setTimeout(function(){
APP.store.dispatch(hideRecordingLabel());
@ -387,8 +358,6 @@ var Recording = {
key: this.recordingPendingKey
};
this._setToolbarButtonToggled(false);
break;
}
@ -398,7 +367,7 @@ var Recording = {
key: this.recordingErrorKey
};
this._setToolbarButtonToggled(false);
recordingButtonToggled = false;
break;
}
@ -412,6 +381,7 @@ var Recording = {
APP.store.dispatch(updateRecordingState({
labelDisplayConfiguration,
recordingButtonToggled,
recordingState
}));
},
@ -431,7 +401,7 @@ var Recording = {
*
* @returns {void}
*/
_onToolbarButtonClick() {
toggleRecording() {
if (dialog) {
return;
}
@ -451,7 +421,12 @@ var Recording = {
}
case Status.AVAILABLE:
case Status.OFF: {
if (this.recordingType === 'jibri')
if (this.recordingType === 'jibri_file') {
this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED);
JitsiMeetJS.analytics.sendEvent('recording.started');
}
else if (this.recordingType === 'jibri')
_requestLiveStreamId().then(streamId => {
this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED,
@ -508,16 +483,6 @@ var Recording = {
);
}
}
},
/**
* Sets the toggled state of the recording toolbar button.
*
* @param {boolean} isToggled indicates if the button should be toggled
* or not
*/
_setToolbarButtonToggled(isToggled) {
$("#toolbar_button_record").toggleClass("toggled", isToggled);
}
};

View File

@ -5,6 +5,7 @@ import React, { Component } from 'react';
import { Watermarks } from '../../base/react';
import { VideoQualityLabel } from '../../video-quality';
import { RecordingLabel } from '../../recording';
import { TranscriptionLabel } from '../../transcription';
declare var interfaceConfig: Object;
@ -70,6 +71,7 @@ export default class LargeVideo extends Component {
<span id = 'localConnectionMessage' />
{ interfaceConfig.filmStripOnly ? null : <VideoQualityLabel /> }
<RecordingLabel />
<TranscriptionLabel />
</div>
);
}

View File

@ -0,0 +1,307 @@
import AKInlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ToggleStateless } from '@atlaskit/toggle';
import { translate } from '../../base/i18n';
import { isButtonEnabled, ToolbarButton } from '../../toolbox';
import Recording from '../../../../modules/UI/recording/Recording';
const DEFAULT_BUTTON_CONFIGURATION = {
buttonName: 'recording',
classNames: [ 'button', 'icon-recEnable' ],
enabled: true,
id: 'toolbar_button_record',
tooltipKey: 'recording.buttonTooltip'
};
/**
* TOFIX: Copy paste from VideoQualityButton, we need a base class that supports
* inline dialogs and does that position thing.
* @type {{bottom: string, left: string, right: string, top: string}}
*/
const TOOLTIP_TO_DIALOG_POSITION = {
bottom: 'bottom center',
left: 'left middle',
right: 'right middle',
top: 'top center'
};
/**
* React {@code Component} for displaying an inline dialog for changing receive
* video settings.
*
* @extends Component
*/
class RecordingButton extends Component {
/**
* {@code RecordingButton}'s property types.
*
* @static
*/
static propTypes = {
/**
* The redux store representation of the JitsiConference.
*/
_conference: React.PropTypes.object,
/**
* Indicates if the recording button has been toggled.
*/
_recordingButtonToggled: React.PropTypes.bool,
/**
* Indicates if the audio recording option should be enabled.
*/
_recordingEnabled: React.PropTypes.bool,
/**
* Indicates if the transcription option should be enabled.
*/
_transcriptionEnabled: React.PropTypes.bool,
/**
* Whether or not the button is visible, based on the visibility of the
* toolbar. Used to automatically hide the inline dialog if not visible.
*/
_visible: React.PropTypes.bool,
/**
* Invoked to obtain translated string.
*/
t: React.PropTypes.func,
/**
* From which side tooltips should display. Will be re-used for
* displaying the inline dialog for video quality adjustment.
*/
tooltipPosition: React.PropTypes.string
};
/**
* Initializes a new {@code RecordingButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
/**
* Whether or not the inline dialog for adjusting received video
* quality is displayed.
*/
showRecordingDialog: false,
isToggleRecordingChecked: false,
isToggleTranscriptionChecked: false
};
// Bind event handlers so they are only bound once for every instance.
this._onDialogClose = this._onDialogClose.bind(this);
this._onDialogToggle = this._onDialogToggle.bind(this);
this._onToggleRecordingChange
= this._onToggleRecordingChange.bind(this);
this._onToggleTranscriptionChange
= this._onToggleTranscriptionChange.bind(this);
}
/**
* Updates the toggled state depending on the _recordingButtonToggled
* prop.
*
* @param {Object} nextProps - The props that will be applied after the
* update.
* @inheritdoc
* @returns {void}
*/
componentWillUpdate(nextProps) {
if (nextProps._recordingButtonToggled
!== this.props._recordingButtonToggled
&& nextProps._recordingButtonToggled
!== this.state.isToggleRecordingChecked) {
this.setState({
isToggleRecordingChecked: nextProps._recordingButtonToggled
});
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _visible, tooltipPosition } = this.props;
const buttonConfiguration = {
...DEFAULT_BUTTON_CONFIGURATION,
classNames: [
...DEFAULT_BUTTON_CONFIGURATION.classNames,
this.state.showRecordingDialog ? 'toggled button-active' : ''
]
};
const content = this._renderRecordingMenu();
return (
<AKInlineDialog
content = { content }
isOpen = { _visible && this.state.showRecordingDialog }
onClose = { this._onDialogClose }
position = { TOOLTIP_TO_DIALOG_POSITION[tooltipPosition] }>
<ToolbarButton
button = { buttonConfiguration }
onClick = { this._onDialogToggle }
tooltipPosition = { tooltipPosition } />
</AKInlineDialog>
);
}
/**
* Hides the attached inline dialog.
*
* @private
* @returns {void}
*/
_onDialogClose() {
this.setState({ showRecordingDialog: false });
}
/**
* Toggles the display of the dialog.
*
* @private
* @returns {void}
*/
_onDialogToggle() {
this.setState({
showRecordingDialog: !this.state.showRecordingDialog
});
}
/**
* Updates the current known state of the toggle selection.
*
* @param {Object} event - The DOM event from changing the toggle selection.
* @private
* @returns {void}
*/
_onToggleRecordingChange(event) {
this.setState({
isToggleRecordingChecked: event.target.checked
});
Recording.toggleRecording();
}
/**
* Updates the current known state of the toggle selection.
*
* @param {Object} event - The DOM event from changing the toggle selection.
* @private
* @returns {void}
*/
_onToggleTranscriptionChange(event) {
const checked = event.target.checked;
this.setState({
isToggleTranscriptionChecked: checked
});
checked
? this.props._conference.startTranscriber()
: this.props._conference.stopTranscriber();
}
/**
* Creates a new {@code RemoteVideoMenu} with buttons for interacting with
* the remote participant.
*
* @private
* @returns {ReactElement}
*/
_renderRecordingMenu() {
const {
_recordingEnabled,
_transcriptionEnabled
} = this.props;
const buttons = [];
if (_recordingEnabled) {
buttons.push(
<div key = 'recordingKey'>
<ToggleStateless
isChecked
= { this.state.isToggleRecordingChecked }
label = 'Recording'
onChange = { this._onToggleRecordingChange } />
<span className = 'recording-popup-item'>
{ this.props.t('recording.videoRecordingLabel') }
</span>
</div>
);
}
if (_transcriptionEnabled) {
buttons.push(
<div key = 'transcriptionKey'>
<ToggleStateless
isChecked
= { this.state.isToggleTranscriptionChecked }
label = 'Transcription'
onChange = { this._onToggleTranscriptionChange } />
<span className = 'recording-popup-item'>
{ this.props.t('recording.transcriptionLabel') }
</span>
</div>
);
}
if (buttons.length > 0) {
return (
<div className = 'recording-popup-dialog'>
<h4 className = 'recording-popup-title'>
{ this.props.t('recording.recordingPopupTitle') }
</h4>
{ buttons }
</div>
);
}
return null;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code VideoQualityButton}
* component's props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _visible: boolean
* }}
*/
function _mapStateToProps(state) {
const { conference } = state['features/base/conference'];
const { enableRecording, enableUserRolesBasedOnToken }
= state['features/base/config'];
const { isGuest } = state['features/jwt'];
const { recordingButtonToggled } = state['features/recording'];
return {
_conference: conference,
_visible: state['features/toolbox'].visible,
_recordingButtonToggled: recordingButtonToggled,
_recordingEnabled: isButtonEnabled('recording')
&& enableRecording
&& conference && conference.isRecordingSupported(),
_transcriptionEnabled: isButtonEnabled('transcription')
&& (!enableUserRolesBasedOnToken || !isGuest)
};
}
export default translate(connect(_mapStateToProps)(RecordingButton));

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import { translate } from '../../base/i18n';
import { shouldRemoteVideosBeVisible } from '../../filmstrip';
@ -45,6 +46,11 @@ class RecordingLabel extends Component {
*/
_remoteVideosVisible: React.PropTypes.bool,
/**
* Indicates if the transcription label is currently visible.
*/
_transcriptionLabelVisible: React.PropTypes.bool,
/**
* Invoked to obtain translated string.
*/
@ -110,10 +116,26 @@ class RecordingLabel extends Component {
? 'with-remote-videos' : 'without-remote-videos'
].join(' ');
let rightIndent = {};
if (this.props._filmstripVisible
&& this.props._remoteVideosVisible) {
rightIndent
= this.props._transcriptionLabelVisible ? '243px' : '200px';
} else {
rightIndent
= this.props._transcriptionLabelVisible ? '123px' : '80px';
}
const inlineStyle = {
right: rightIndent
};
return (
<span
className = { rootClassName }
id = 'recordingLabel'>
id = 'recordingLabel'
style = { inlineStyle }>
<span id = 'recordingLabelText'>
{ this.props.t(key) }
</span>
@ -143,6 +165,7 @@ class RecordingLabel extends Component {
function _mapStateToProps(state) {
const { visible } = state['features/filmstrip'];
const { labelDisplayConfiguration } = state['features/recording'];
const { transcriptionState } = state['features/transcription'];
return {
/**
@ -165,7 +188,10 @@ function _mapStateToProps(state) {
*
* @type {boolean}
*/
_remoteVideosVisible: shouldRemoteVideosBeVisible(state)
_remoteVideosVisible: shouldRemoteVideosBeVisible(state),
_transcriptionLabelVisible: transcriptionState
=== JitsiMeetJS.constants.transcriptionStatus.ON
};
}

View File

@ -1 +1,2 @@
export { default as RecordingLabel } from './RecordingLabel';
export { default as RecordingButton } from './RecordingButton';

View File

@ -6,6 +6,7 @@ import { DEFAULT_AVATAR_RELATIVE_PATH } from '../base/participants';
import { openDeviceSelectionDialog } from '../device-selection';
import { openDialOutDialog } from '../dial-out';
import { openAddPeopleDialog, openInviteDialog } from '../invite';
import { RecordingButton } from '../recording';
import { VideoQualityButton } from '../video-quality';
import UIEvents from '../../../service/UI/UIEvents';
@ -368,13 +369,7 @@ const buttons: Object = {
* initialization in the recording module.
*/
recording: {
classNames: [ 'button' ],
enabled: true,
// will be displayed once the recording functionality is detected
hidden: true,
id: 'toolbar_button_record',
tooltipKey: 'liveStreaming.buttonTooltip'
component: RecordingButton
},
/**

View File

@ -0,0 +1,12 @@
/**
* The type of Redux action which updates the current known state of the
* transcription feature.
*
* {
* type: TRANSCRIPTION_STATE_UPDATED,
* recordingState: string
* }
* @public
*/
export const TRANSCRIPTION_STATE_UPDATED
= Symbol('TRANSCRIPTION_STATE_UPDATED');

View File

@ -0,0 +1,22 @@
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { TRANSCRIPTION_STATE_UPDATED } from './actionTypes';
/**
* Updates the Redux state for the transcription feature.
*
* @param {Object} transcriptionState - The new state to merge with the existing
* state in Redux.
* @returns {{
* type: TRANSCRIPTION_STATE_UPDATED,
* recordingState: string
* }}
*/
export function updateTranscriptionState(transcriptionState
= JitsiMeetJS.constants.transcriptionStatus.OFF) {
return {
type: TRANSCRIPTION_STATE_UPDATED,
transcriptionState
};
}

View File

@ -0,0 +1,3 @@
/**
* Created by ystamcheva on 12/9/17.
*/

View File

@ -0,0 +1,145 @@
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { shouldRemoteVideosBeVisible } from '../../filmstrip';
/**
* React {@code Component} responsible for displaying a label that indicates
* the transcription current state.
*/
export class TranscriptionLabel extends Component {
/**
* {@code VideoQualityLabel}'s property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the filmstrip is currently set to be displayed.
*/
_filmstripVisible: React.PropTypes.bool,
/**
* Whether or not remote videos within the filmstrip are currently
* visible. Depending on the visibility state, coupled with filmstrip
* visibility, CSS classes will be set to allow for adjusting of
* {@code TranscriptionLabel} positioning.
*/
_remoteVideosVisible: React.PropTypes.bool,
/**
* The current state of the transcription.
*/
_transcriptionState: React.PropTypes.string
};
/**
* Initializes a new {@code TranscriptionLabel} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
/**
* Whether or not the filmstrip was not visible but has transitioned
* in the latest component update to visible. This boolean is used
* to set a class for position animations.
*
* @type {boolean}
*/
filmstripBecomingVisible: false
};
}
/**
* Updates the state for whether or not the filmstrip is being toggled to
* display after having being hidden.
*
* @inheritdoc
* @param {Object} nextProps - The read-only props which this Component will
* receive.
* @returns {void}
*/
componentWillReceiveProps(nextProps) {
this.setState({
filmstripBecomingVisible: nextProps._filmstripVisible
&& !this.props._filmstripVisible
});
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const isVisible = this.props._transcriptionState
=== JitsiMeetJS.constants.transcriptionStatus.ON;
const rootClassName = [
'video-state-indicator moveToCorner',
isVisible ? 'show-inline' : '',
this.state.filmstripBecomingVisible ? 'opening' : '',
this.props._filmstripVisible
? 'with-filmstrip' : 'without-filmstrip',
this.props._remoteVideosVisible
? 'with-remote-videos' : 'without-remote-videos'
].join(' ');
return (
isVisible
? <span
className = { rootClassName }
id = 'transcriptionLabel'>
<span className = 'icon-edit' />
</span>
: null
);
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code TranscriptionLabel}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _filmstripVisible: boolean,
* _remoteVideosVisible: boolean,
* _transcriptionState: string
* }}
*/
function _mapStateToProps(state) {
const { visible } = state['features/filmstrip'];
const { transcriptionState } = state['features/transcription'];
return {
/**
* Whether or not the filmstrip is currently set to be displayed.
*
* @type {boolean}
*/
_filmstripVisible: visible,
/**
* Whether or not remote videos are displayed in the filmstrip.
*
* @type {boolean}
*/
_remoteVideosVisible: shouldRemoteVideosBeVisible(state),
/**
* The current state of the transcription.
*/
_transcriptionState: transcriptionState
};
}
export default connect(_mapStateToProps)(TranscriptionLabel);

View File

@ -0,0 +1 @@
export { default as TranscriptionLabel } from './TranscriptionLabel';

View File

@ -0,0 +1,5 @@
export * from './actions';
export * from './components';
import './middleware';
import './reducer';

View File

@ -0,0 +1,40 @@
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference';
import { MiddlewareRegistry } from '../base/redux';
import { updateTranscriptionState } from './actions';
/**
* Middleware that captures conference actions and adds a listener to
* transcription status events.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case CONFERENCE_JOINED: {
const { conference } = action;
conference && conference.on(
JitsiConferenceEvents.TRANSCRIPTION_STATUS_CHANGED,
(...args) => store.dispatch(updateTranscriptionState(...args)));
break;
}
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
// REMOVE THE LISTENER?
break;
}
return result;
});

View File

@ -0,0 +1,18 @@
import { ReducerRegistry } from '../base/redux';
import { TRANSCRIPTION_STATE_UPDATED } from './actionTypes';
/**
* Reduces the Redux actions of the feature features/transcription.
*/
ReducerRegistry.register('features/transcription', (state = {}, action) => {
switch (action.type) {
case TRANSCRIPTION_STATE_UPDATED:
return {
...state,
transcriptionState: action.transcriptionState
};
default:
return state;
}
});

View File

@ -119,9 +119,9 @@ class VideoQualityDialog extends Component {
return (
<div className = 'video-quality-dialog'>
<h3 className = 'video-quality-dialog-title'>
<h4 className = 'video-quality-dialog-title'>
{ t('videoStatus.callQuality') }
</h3>
</h4>
<div className = { showP2PWarning ? '' : 'hide-warning' }>
{ this._renderP2PMessage() }
</div>