feat(RN): displays transcription subtitles
This commit is contained in:
parent
eac069c930
commit
26d906fa46
|
@ -17,6 +17,7 @@ import {
|
||||||
participantRoleChanged,
|
participantRoleChanged,
|
||||||
participantUpdated
|
participantUpdated
|
||||||
} from '../participants';
|
} from '../participants';
|
||||||
|
import { endpointMessageReceived } from '../../subtitles';
|
||||||
import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
|
import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
|
||||||
import { getJitsiMeetGlobalNS } from '../util';
|
import { getJitsiMeetGlobalNS } from '../util';
|
||||||
|
|
||||||
|
@ -137,6 +138,10 @@ function _addConferenceListeners(conference, dispatch) {
|
||||||
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
||||||
id => dispatch(dominantSpeakerChanged(id, conference)));
|
id => dispatch(dominantSpeakerChanged(id, conference)));
|
||||||
|
|
||||||
|
conference.on(
|
||||||
|
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||||
|
(...args) => dispatch(endpointMessageReceived(...args)));
|
||||||
|
|
||||||
conference.on(
|
conference.on(
|
||||||
JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
|
JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
|
||||||
(...args) => dispatch(participantConnectionStatusChanged(...args)));
|
(...args) => dispatch(participantConnectionStatusChanged(...args)));
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { FILMSTRIP_SIZE, Filmstrip, isFilmstripVisible } from '../../filmstrip';
|
||||||
import { LargeVideo } from '../../large-video';
|
import { LargeVideo } from '../../large-video';
|
||||||
import { CalleeInfoContainer } from '../../invite';
|
import { CalleeInfoContainer } from '../../invite';
|
||||||
import { NotificationsContainer } from '../../notifications';
|
import { NotificationsContainer } from '../../notifications';
|
||||||
|
import { Captions } from '../../subtitles';
|
||||||
import { setToolboxVisible, Toolbox } from '../../toolbox';
|
import { setToolboxVisible, Toolbox } from '../../toolbox';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
@ -283,18 +284,20 @@ class Conference extends Component<Props> {
|
||||||
<View
|
<View
|
||||||
pointerEvents = 'box-none'
|
pointerEvents = 'box-none'
|
||||||
style = { styles.toolboxAndFilmstripContainer }>
|
style = { styles.toolboxAndFilmstripContainer }>
|
||||||
{
|
{/*
|
||||||
|
* Notifications are rendered on the very top of other
|
||||||
/**
|
* components like subtitles, toolbox and filmstrip.
|
||||||
* Notifications are rendered on the very top of other
|
*/
|
||||||
* components like subtitles, toolbox and filmstrip.
|
|
||||||
*/
|
|
||||||
this._renderNotificationsContainer()
|
this._renderNotificationsContainer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<Captions onPress = { this._onClick } />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
* The Toolbox is in a stacking layer bellow the Filmstrip.
|
* The Toolbox is in a stacking layer bellow the Filmstrip.
|
||||||
*/}
|
*/}
|
||||||
<Toolbox />
|
<Toolbox />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
* The Filmstrip is in a stacking layer above the
|
* The Filmstrip is in a stacking layer above the
|
||||||
* LargeVideo. The LargeVideo and the Filmstrip form what
|
* LargeVideo. The LargeVideo and the Filmstrip form what
|
||||||
|
@ -369,28 +372,26 @@ class Conference extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a container for notifications to be displayed by
|
* Renders a container for notifications to be displayed by the
|
||||||
* the base/notifications feature.
|
* base/notifications feature.
|
||||||
*
|
*
|
||||||
* @returns {React$Element}
|
|
||||||
* @private
|
* @private
|
||||||
|
* @returns {React$Element}
|
||||||
*/
|
*/
|
||||||
_renderNotificationsContainer() {
|
_renderNotificationsContainer() {
|
||||||
const notificationsStyle = { };
|
const notificationsStyle = {};
|
||||||
|
|
||||||
/**
|
// In the landscape mode (wide) there's problem with notifications being
|
||||||
* In the landscape mode (wide) there's problem with notifications being
|
// shadowed by the filmstrip rendered on the right. This makes the "x"
|
||||||
* shadowed by the filmstrip rendered on the right. This makes the "x"
|
// button not clickable. In order to avoid that a margin of the
|
||||||
* button not clickable. In order to avoid that a margin of
|
// filmstrip's size is added to the right.
|
||||||
* the filmstrip's size is added to the right.
|
//
|
||||||
*
|
// Pawel: after many attempts I failed to make notifications adjust to
|
||||||
* Pawel: after many attempts I failed to make notifications adjust to
|
// their contents width because of column and rows being used in the
|
||||||
* their contents width because of column and rows being used in
|
// flex layout. The only option that seemed to limit the notification's
|
||||||
* the flex layout. The only option that seemed to limit
|
// size was explicit 'width' value which is not better than the margin
|
||||||
* the notification's size was explicit 'width' value which is not
|
// added here.
|
||||||
* better than the margin added here.
|
if (this.props._filmstripVisible && !isNarrowAspectRatio(this)) {
|
||||||
*/
|
|
||||||
if (!isNarrowAspectRatio(this) && this.props._filmstripVisible) {
|
|
||||||
notificationsStyle.marginRight = FILMSTRIP_SIZE;
|
notificationsStyle.marginRight = FILMSTRIP_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,37 @@
|
||||||
/* @flow */
|
// @flow
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { Watermarks } from '../../base/react';
|
import { Watermarks } from '../../base/react';
|
||||||
import { TranscriptionSubtitles } from '../../subtitles/';
|
import { Captions } from '../../subtitles/';
|
||||||
|
|
||||||
import Labels from './Labels';
|
import Labels from './Labels';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code Component} props of {@link LargeVideo}.
|
||||||
|
*/
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the {@code VideoQualityLabel} should not be displayed.
|
||||||
|
*/
|
||||||
|
hideVideoQualityLabel: boolean
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@link Component} which represents the large video (a.k.a.
|
* Implements a React {@link Component} which represents the large video (a.k.a.
|
||||||
* the conference participant who is on the local stage) on Web/React.
|
* the conference participant who is on the local stage) on Web/React.
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class LargeVideo extends Component<*> {
|
export default class LargeVideo extends Component<Props> {
|
||||||
static propTypes = {
|
|
||||||
/**
|
|
||||||
* True if the {@code VideoQualityLabel} should not be displayed.
|
|
||||||
*/
|
|
||||||
hideVideoQualityLabel: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
* @returns {ReactElement}
|
* @returns {React$Element}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
@ -52,18 +55,15 @@ export default class LargeVideo extends Component<*> {
|
||||||
<span id = 'remoteConnectionMessage' />
|
<span id = 'remoteConnectionMessage' />
|
||||||
<div id = 'largeVideoElementsContainer'>
|
<div id = 'largeVideoElementsContainer'>
|
||||||
<div id = 'largeVideoBackgroundContainer' />
|
<div id = 'largeVideoBackgroundContainer' />
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
{/*
|
||||||
* FIXME: the architecture of elements related to the
|
* FIXME: the architecture of elements related to the large
|
||||||
* large video and the naming. The background is not
|
* video and the naming. The background is not part of
|
||||||
* part of largeVideoWrapper because we are controlling
|
* largeVideoWrapper because we are controlling the size of
|
||||||
* the size of the video through largeVideoWrapper.
|
* the video through largeVideoWrapper. That's why we need
|
||||||
* That's why we need another container for the the
|
* another container for the background and the
|
||||||
* background and the largeVideoWrapper in order to
|
* largeVideoWrapper in order to hide/show them.
|
||||||
* hide/show them.
|
*/}
|
||||||
*/
|
|
||||||
}
|
|
||||||
<div id = 'largeVideoWrapper'>
|
<div id = 'largeVideoWrapper'>
|
||||||
<video
|
<video
|
||||||
autoPlay = { true }
|
autoPlay = { true }
|
||||||
|
@ -72,10 +72,10 @@ export default class LargeVideo extends Component<*> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|
||||||
? null : <TranscriptionSubtitles /> }
|
|| <Captions /> }
|
||||||
<span id = 'localConnectionMessage' />
|
<span id = 'localConnectionMessage' />
|
||||||
{ this.props.hideVideoQualityLabel
|
{ this.props.hideVideoQualityLabel
|
||||||
? null : <Labels /> }
|
|| <Labels /> }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { Component } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code AbstractCaptions} properties.
|
||||||
|
*/
|
||||||
|
export type AbstractCaptionsProps = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether local participant is requesting to see subtitles.
|
||||||
|
*/
|
||||||
|
_requestingSubtitles: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transcript texts formatted with participant's name and final content.
|
||||||
|
* Mapped by id just to have the keys for convenience during the rendering
|
||||||
|
* process.
|
||||||
|
*/
|
||||||
|
_transcripts: Map<string, string>
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract React {@code Component} which can display speech-to-text results
|
||||||
|
* from Jigasi as subtitles.
|
||||||
|
*/
|
||||||
|
export class AbstractCaptions<P: AbstractCaptionsProps>
|
||||||
|
extends Component<P> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {React$Element}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _requestingSubtitles, _transcripts } = this.props;
|
||||||
|
|
||||||
|
if (!_requestingSubtitles || !_transcripts.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paragraphs = [];
|
||||||
|
|
||||||
|
for (const [ id, text ] of _transcripts) {
|
||||||
|
paragraphs.push(this._renderParagraph(id, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._renderSubtitlesContainer(paragraphs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the transcription text.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {string} id - The ID of the transcript message from which the
|
||||||
|
* {@code text} has been created.
|
||||||
|
* @param {string} text - Subtitles text formatted with the participant's
|
||||||
|
* name.
|
||||||
|
* @protected
|
||||||
|
* @returns {React$Element} - The React element which displays the text.
|
||||||
|
*/
|
||||||
|
_renderParagraph: (id: string, text: string) => React$Element<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the subtitles container.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {Array<React$Element>} paragraphs - An array of elements created
|
||||||
|
* for each subtitle using the {@link _renderParagraph} method.
|
||||||
|
* @protected
|
||||||
|
* @returns {React$Element} - The subtitles container.
|
||||||
|
*/
|
||||||
|
_renderSubtitlesContainer: (Array<React$Element<*>>) => React$Element<*>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the transcript messages into text by prefixing participant's name to
|
||||||
|
* avoid duplicating the effort on platform specific component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @private
|
||||||
|
* @returns {Map<string, string>} - Formatted transcript subtitles mapped by
|
||||||
|
* transcript message IDs.
|
||||||
|
*/
|
||||||
|
function _constructTranscripts(state: Object): Map<string, string> {
|
||||||
|
const { _transcriptMessages } = state['features/subtitles'];
|
||||||
|
const transcripts = new Map();
|
||||||
|
|
||||||
|
for (const [ id, transcriptMessage ] of _transcriptMessages) {
|
||||||
|
if (transcriptMessage) {
|
||||||
|
let text = `${transcriptMessage.participantName}: `;
|
||||||
|
|
||||||
|
if (transcriptMessage.final) {
|
||||||
|
text += transcriptMessage.final;
|
||||||
|
} else {
|
||||||
|
const stable = transcriptMessage.stable || '';
|
||||||
|
const unstable = transcriptMessage.unstable || '';
|
||||||
|
|
||||||
|
text += stable + unstable;
|
||||||
|
}
|
||||||
|
|
||||||
|
transcripts.set(id, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transcripts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the transcriptionSubtitles in the redux state to the associated props of
|
||||||
|
* {@code AbstractCaptions}.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @private
|
||||||
|
* @returns {{
|
||||||
|
* _requestingSubtitles: boolean,
|
||||||
|
* _transcripts: Map<string, string>
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function _abstractMapStateToProps(state: Object) {
|
||||||
|
const { _requestingSubtitles } = state['features/subtitles'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
_requestingSubtitles,
|
||||||
|
_transcripts: _constructTranscripts(state)
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { Container, Text } from '../../base/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
_abstractMapStateToProps,
|
||||||
|
AbstractCaptions,
|
||||||
|
type AbstractCaptionsProps
|
||||||
|
} from './AbstractCaptions';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code Component} props of {@link Captions}.
|
||||||
|
*/
|
||||||
|
type Props = AbstractCaptionsProps & {
|
||||||
|
onPress: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React {@code Component} which can display speech-to-text results from
|
||||||
|
* Jigasi as subtitles.
|
||||||
|
*/
|
||||||
|
class Captions
|
||||||
|
extends AbstractCaptions<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the transcription text.
|
||||||
|
*
|
||||||
|
* @param {string} id - The ID of the transcript message from which the
|
||||||
|
* {@code text} has been created.
|
||||||
|
* @param {string} text - Subtitles text formatted with the participant's
|
||||||
|
* name.
|
||||||
|
* @protected
|
||||||
|
* @returns {React$Element} - The React element which displays the text.
|
||||||
|
*/
|
||||||
|
_renderParagraph(id: string, text: string): React$Element<*> {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key = { id }
|
||||||
|
onPress = { this.props.onPress }
|
||||||
|
style = { styles.subtitle } >
|
||||||
|
{ text }
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the subtitles container.
|
||||||
|
*
|
||||||
|
* @param {Array<React$Element>} paragraphs - An array of elements created
|
||||||
|
* for each subtitle using the {@link _renderParagraph} method.
|
||||||
|
* @protected
|
||||||
|
* @returns {React$Element} - The subtitles container.
|
||||||
|
*/
|
||||||
|
_renderSubtitlesContainer(
|
||||||
|
paragraphs: Array<React$Element<*>>): React$Element<*> {
|
||||||
|
return (
|
||||||
|
<Container style = { styles.subtitlesContainer } >
|
||||||
|
{ paragraphs }
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(_abstractMapStateToProps)(Captions);
|
|
@ -0,0 +1,55 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
_abstractMapStateToProps,
|
||||||
|
AbstractCaptions,
|
||||||
|
type AbstractCaptionsProps as Props
|
||||||
|
} from './AbstractCaptions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React {@code Component} which can display speech-to-text results from
|
||||||
|
* Jigasi as subtitles.
|
||||||
|
*/
|
||||||
|
class Captions
|
||||||
|
extends AbstractCaptions<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the transcription text.
|
||||||
|
*
|
||||||
|
* @param {string} id - The ID of the transcript message from which the
|
||||||
|
* {@code text} has been created.
|
||||||
|
* @param {string} text - Subtitles text formatted with the participant's
|
||||||
|
* name.
|
||||||
|
* @protected
|
||||||
|
* @returns {React$Element} - The React element which displays the text.
|
||||||
|
*/
|
||||||
|
_renderParagraph(id: string, text: string): React$Element<*> {
|
||||||
|
return (
|
||||||
|
<p key = { id }>
|
||||||
|
<span>{ text }</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the subtitles container.
|
||||||
|
*
|
||||||
|
* @param {Array<React$Element>} paragraphs - An array of elements created
|
||||||
|
* for each subtitle using the {@link _renderParagraph} method.
|
||||||
|
* @protected
|
||||||
|
* @returns {React$Element} - The subtitles container.
|
||||||
|
*/
|
||||||
|
_renderSubtitlesContainer(
|
||||||
|
paragraphs: Array<React$Element<*>>): React$Element<*> {
|
||||||
|
return (
|
||||||
|
<div className = 'transcription-subtitles' >
|
||||||
|
{ paragraphs }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(_abstractMapStateToProps)(Captions);
|
|
@ -1,96 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of the React {@code Component} props of
|
|
||||||
* {@link TranscriptionSubtitles}.
|
|
||||||
*/
|
|
||||||
type Props = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of transcriptMessageID's with corresponding transcriptMessage.
|
|
||||||
*/
|
|
||||||
_transcriptMessages: Map<string, Object>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether local participant is requesting to see subtitles
|
|
||||||
*/
|
|
||||||
_requestingSubtitles: Boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React {@code Component} which can display speech-to-text results from
|
|
||||||
* Jigasi as subtitles.
|
|
||||||
*/
|
|
||||||
class TranscriptionSubtitles extends Component<Props> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements React's {@link Component#render()}.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
if (!this.props._requestingSubtitles
|
|
||||||
|| !this.props._transcriptMessages) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const paragraphs = [];
|
|
||||||
|
|
||||||
for (const [ transcriptMessageID, transcriptMessage ]
|
|
||||||
of this.props._transcriptMessages) {
|
|
||||||
let text;
|
|
||||||
|
|
||||||
if (transcriptMessage) {
|
|
||||||
text = `${transcriptMessage.participantName}: `;
|
|
||||||
|
|
||||||
if (transcriptMessage.final) {
|
|
||||||
text += transcriptMessage.final;
|
|
||||||
} else {
|
|
||||||
const stable = transcriptMessage.stable || '';
|
|
||||||
const unstable = transcriptMessage.unstable || '';
|
|
||||||
|
|
||||||
text += stable + unstable;
|
|
||||||
}
|
|
||||||
paragraphs.push(
|
|
||||||
<p key = { transcriptMessageID }>
|
|
||||||
<span>{ text }</span>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className = 'transcription-subtitles' >
|
|
||||||
{ paragraphs }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the transcriptionSubtitles in the Redux state to the associated
|
|
||||||
* props of {@code TranscriptionSubtitles}.
|
|
||||||
*
|
|
||||||
* @param {Object} state - The Redux state.
|
|
||||||
* @private
|
|
||||||
* @returns {{
|
|
||||||
* _transcriptMessages: Map
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
function _mapStateToProps(state) {
|
|
||||||
const {
|
|
||||||
_transcriptMessages,
|
|
||||||
_requestingSubtitles
|
|
||||||
} = state['features/subtitles'];
|
|
||||||
|
|
||||||
return {
|
|
||||||
_transcriptMessages,
|
|
||||||
_requestingSubtitles
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export default connect(_mapStateToProps)(TranscriptionSubtitles);
|
|
|
@ -1,2 +1,2 @@
|
||||||
export { default as TranscriptionSubtitles } from './TranscriptionSubtitles';
|
export { default as Captions } from './Captions';
|
||||||
export { default as ClosedCaptionButton } from './ClosedCaptionButton';
|
export { default as ClosedCaptionButton } from './ClosedCaptionButton';
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { BoxModel, ColorPalette, createStyleSheet } from '../../base/styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The styles of the React {@code Component}s of the feature subtitles.
|
||||||
|
*/
|
||||||
|
export default createStyleSheet({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style for subtitle paragraph.
|
||||||
|
*/
|
||||||
|
subtitle: {
|
||||||
|
backgroundColor: ColorPalette.black,
|
||||||
|
borderRadius: BoxModel.margin / 4,
|
||||||
|
color: ColorPalette.white,
|
||||||
|
marginBottom: BoxModel.margin,
|
||||||
|
padding: BoxModel.padding / 2
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style for the subtitles container.
|
||||||
|
*/
|
||||||
|
subtitlesContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 0,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
margin: BoxModel.margin
|
||||||
|
}
|
||||||
|
});
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
import {
|
|
||||||
ENDPOINT_MESSAGE_RECEIVED,
|
|
||||||
TOGGLE_REQUESTING_SUBTITLES
|
|
||||||
} from './actionTypes';
|
|
||||||
import {
|
import {
|
||||||
removeTranscriptMessage,
|
removeTranscriptMessage,
|
||||||
updateTranscriptMessage
|
updateTranscriptMessage
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
import {
|
||||||
|
ENDPOINT_MESSAGE_RECEIVED,
|
||||||
|
TOGGLE_REQUESTING_SUBTITLES
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
|
@ -25,34 +25,35 @@ const JSON_TYPE_TRANSCRIPTION_RESULT = 'transcription-result';
|
||||||
*/
|
*/
|
||||||
const JSON_TYPE_TRANSLATION_RESULT = 'translation-result';
|
const JSON_TYPE_TRANSLATION_RESULT = 'translation-result';
|
||||||
|
|
||||||
/**
|
|
||||||
* The local participant property which is used to store the language
|
|
||||||
* preference for translation for a participant.
|
|
||||||
*/
|
|
||||||
const P_NAME_TRANSLATION_LANGUAGE = 'translation_language';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The local participant property which is used to set whether the local
|
* The local participant property which is used to set whether the local
|
||||||
* participant wants to have a transcriber in the room.
|
* participant wants to have a transcriber in the room.
|
||||||
*/
|
*/
|
||||||
const P_NAME_REQUESTING_TRANSCRIPTION = 'requestingTranscription';
|
const P_NAME_REQUESTING_TRANSCRIPTION = 'requestingTranscription';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local participant property which is used to store the language
|
||||||
|
* preference for translation for a participant.
|
||||||
|
*/
|
||||||
|
const P_NAME_TRANSLATION_LANGUAGE = 'translation_language';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time after which the rendered subtitles will be removed.
|
* Time after which the rendered subtitles will be removed.
|
||||||
*/
|
*/
|
||||||
const REMOVE_AFTER_MS = 3000;
|
const REMOVE_AFTER_MS = 3000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware that catches actions related to transcript messages
|
* Middleware that catches actions related to transcript messages to be rendered
|
||||||
* to be rendered in {@link TranscriptionSubtitles }
|
* in {@link Captions}.
|
||||||
*
|
*
|
||||||
* @param {Store} store - Redux store.
|
* @param {Store} store - The redux store.
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
MiddlewareRegistry.register(store => next => action => {
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ENDPOINT_MESSAGE_RECEIVED:
|
case ENDPOINT_MESSAGE_RECEIVED:
|
||||||
return _endpointMessageReceived(store, next, action);
|
return _endpointMessageReceived(store, next, action);
|
||||||
|
|
||||||
case TOGGLE_REQUESTING_SUBTITLES:
|
case TOGGLE_REQUESTING_SUBTITLES:
|
||||||
_requestingSubtitlesToggled(store);
|
_requestingSubtitlesToggled(store);
|
||||||
break;
|
break;
|
||||||
|
@ -61,22 +62,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
return next(action);
|
return next(action);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the local property 'requestingTranscription'. This will cause Jicofo
|
|
||||||
* and Jigasi to decide whether the transcriber needs to be in the room.
|
|
||||||
*
|
|
||||||
* @param {Store} store - The redux store.
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function _requestingSubtitlesToggled({ getState }) {
|
|
||||||
const { _requestingSubtitles } = getState()['features/subtitles'];
|
|
||||||
const { conference } = getState()['features/base/conference'];
|
|
||||||
|
|
||||||
conference.setLocalParticipantProperty(P_NAME_REQUESTING_TRANSCRIPTION,
|
|
||||||
!_requestingSubtitles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies the feature transcription that the action
|
* Notifies the feature transcription that the action
|
||||||
* {@code ENDPOINT_MESSAGE_RECEIVED} is being dispatched within a specific redux
|
* {@code ENDPOINT_MESSAGE_RECEIVED} is being dispatched within a specific redux
|
||||||
|
@ -92,84 +77,81 @@ function _requestingSubtitlesToggled({ getState }) {
|
||||||
* @returns {Object} The value returned by {@code next(action)}.
|
* @returns {Object} The value returned by {@code next(action)}.
|
||||||
*/
|
*/
|
||||||
function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||||
if (!(action.json
|
const { json } = action;
|
||||||
&& (action.json.type === JSON_TYPE_TRANSCRIPTION_RESULT
|
|
||||||
|| action.json.type === JSON_TYPE_TRANSLATION_RESULT))) {
|
if (!(json
|
||||||
|
&& (json.type === JSON_TYPE_TRANSCRIPTION_RESULT
|
||||||
|
|| json.type === JSON_TYPE_TRANSLATION_RESULT))) {
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = action.json;
|
const state = getState();
|
||||||
const translationLanguage
|
const translationLanguage
|
||||||
= getState()['features/base/conference'].conference
|
= state['features/base/conference'].conference
|
||||||
.getLocalParticipantProperty(P_NAME_TRANSLATION_LANGUAGE);
|
.getLocalParticipantProperty(P_NAME_TRANSLATION_LANGUAGE);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transcriptMessageID = json.message_id;
|
const transcriptMessageID = json.message_id;
|
||||||
const participantName = json.participant.name;
|
const participantName = json.participant.name;
|
||||||
const isInterim = json.is_interim;
|
|
||||||
const stability = json.stability;
|
|
||||||
|
|
||||||
if (json.type === JSON_TYPE_TRANSLATION_RESULT
|
if (json.type === JSON_TYPE_TRANSLATION_RESULT
|
||||||
&& json.language === translationLanguage) {
|
&& json.language === translationLanguage) {
|
||||||
// Displays final results in the target language if translation is
|
// Displays final results in the target language if translation is
|
||||||
// enabled.
|
// enabled.
|
||||||
|
|
||||||
const newTranscriptMessage = {
|
const newTranscriptMessage = {
|
||||||
participantName,
|
clearTimeOut: undefined,
|
||||||
final: json.text,
|
final: json.text,
|
||||||
clearTimeOut: undefined
|
participantName
|
||||||
};
|
};
|
||||||
|
|
||||||
setClearerOnTranscriptMessage(dispatch,
|
_setClearerOnTranscriptMessage(dispatch,
|
||||||
transcriptMessageID, newTranscriptMessage);
|
transcriptMessageID, newTranscriptMessage);
|
||||||
dispatch(updateTranscriptMessage(transcriptMessageID,
|
dispatch(updateTranscriptMessage(transcriptMessageID,
|
||||||
newTranscriptMessage));
|
newTranscriptMessage));
|
||||||
|
|
||||||
} else if (json.type === JSON_TYPE_TRANSCRIPTION_RESULT
|
} else if (json.type === JSON_TYPE_TRANSCRIPTION_RESULT
|
||||||
&& !translationLanguage) {
|
&& !translationLanguage) {
|
||||||
// Displays interim and final results without any translation if
|
// Displays interim and final results without any translation if
|
||||||
// translations are disabled.
|
// translations are disabled.
|
||||||
|
|
||||||
const text = json.transcript[0].text;
|
const { text } = json.transcript[0];
|
||||||
|
|
||||||
// We update the previous transcript message with the same
|
// We update the previous transcript message with the same
|
||||||
// message ID or adds a new transcript message if it does not
|
// message ID or adds a new transcript message if it does not
|
||||||
// exist in the map.
|
// exist in the map.
|
||||||
const newTranscriptMessage
|
const newTranscriptMessage = {
|
||||||
= { ...getState()['features/subtitles']._transcriptMessages
|
...state['features/subtitles']._transcriptMessages
|
||||||
.get(transcriptMessageID) || { participantName } };
|
.get(transcriptMessageID)
|
||||||
|
|| { participantName }
|
||||||
|
};
|
||||||
|
|
||||||
setClearerOnTranscriptMessage(dispatch,
|
_setClearerOnTranscriptMessage(dispatch,
|
||||||
transcriptMessageID, newTranscriptMessage);
|
transcriptMessageID, newTranscriptMessage);
|
||||||
|
|
||||||
// If this is final result, update the state as a final result
|
// If this is final result, update the state as a final result
|
||||||
// and start a count down to remove the subtitle from the state
|
// and start a count down to remove the subtitle from the state
|
||||||
if (!isInterim) {
|
if (!json.is_interim) {
|
||||||
|
|
||||||
newTranscriptMessage.final = text;
|
newTranscriptMessage.final = text;
|
||||||
|
|
||||||
dispatch(updateTranscriptMessage(transcriptMessageID,
|
} else if (json.stability > 0.85) {
|
||||||
newTranscriptMessage));
|
|
||||||
} else if (stability > 0.85) {
|
|
||||||
|
|
||||||
// If the message has a high stability, we can update the
|
// If the message has a high stability, we can update the
|
||||||
// stable field of the state and remove the previously
|
// stable field of the state and remove the previously
|
||||||
// unstable results
|
// unstable results
|
||||||
|
|
||||||
newTranscriptMessage.stable = text;
|
newTranscriptMessage.stable = text;
|
||||||
newTranscriptMessage.unstable = undefined;
|
newTranscriptMessage.unstable = undefined;
|
||||||
|
|
||||||
dispatch(updateTranscriptMessage(transcriptMessageID,
|
|
||||||
newTranscriptMessage));
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, this result has an unstable result, which we
|
// Otherwise, this result has an unstable result, which we
|
||||||
// add to the state. The unstable result will be appended
|
// add to the state. The unstable result will be appended
|
||||||
// after the stable part.
|
// after the stable part.
|
||||||
|
|
||||||
newTranscriptMessage.unstable = text;
|
newTranscriptMessage.unstable = text;
|
||||||
dispatch(updateTranscriptMessage(transcriptMessageID,
|
|
||||||
newTranscriptMessage));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
updateTranscriptMessage(
|
||||||
|
transcriptMessageID,
|
||||||
|
newTranscriptMessage));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error occurred while updating transcriptions\n', error);
|
logger.error('Error occurred while updating transcriptions\n', error);
|
||||||
|
@ -178,6 +160,24 @@ function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the local property 'requestingTranscription'. This will cause Jicofo
|
||||||
|
* and Jigasi to decide whether the transcriber needs to be in the room.
|
||||||
|
*
|
||||||
|
* @param {Store} store - The redux store.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _requestingSubtitlesToggled({ getState }) {
|
||||||
|
const state = getState();
|
||||||
|
const { _requestingSubtitles } = state['features/subtitles'];
|
||||||
|
const { conference } = state['features/base/conference'];
|
||||||
|
|
||||||
|
conference.setLocalParticipantProperty(
|
||||||
|
P_NAME_REQUESTING_TRANSCRIPTION,
|
||||||
|
!_requestingSubtitles);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a timeout on a TranscriptMessage object so it clears itself when it's not
|
* Set a timeout on a TranscriptMessage object so it clears itself when it's not
|
||||||
* updated.
|
* updated.
|
||||||
|
@ -185,10 +185,9 @@ function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||||
* @param {Function} dispatch - Dispatch remove action to store.
|
* @param {Function} dispatch - Dispatch remove action to store.
|
||||||
* @param {string} transcriptMessageID - The id of the message to remove.
|
* @param {string} transcriptMessageID - The id of the message to remove.
|
||||||
* @param {Object} transcriptMessage - The message to remove.
|
* @param {Object} transcriptMessage - The message to remove.
|
||||||
*
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function setClearerOnTranscriptMessage(
|
function _setClearerOnTranscriptMessage(
|
||||||
dispatch,
|
dispatch,
|
||||||
transcriptMessageID,
|
transcriptMessageID,
|
||||||
transcriptMessage) {
|
transcriptMessage) {
|
||||||
|
@ -196,7 +195,8 @@ function setClearerOnTranscriptMessage(
|
||||||
clearTimeout(transcriptMessage.clearTimeOut);
|
clearTimeout(transcriptMessage.clearTimeOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
transcriptMessage.clearTimeOut = setTimeout(() => {
|
transcriptMessage.clearTimeOut
|
||||||
dispatch(removeTranscriptMessage(transcriptMessageID));
|
= setTimeout(
|
||||||
}, REMOVE_AFTER_MS);
|
() => dispatch(removeTranscriptMessage(transcriptMessageID)),
|
||||||
|
REMOVE_AFTER_MS);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue