You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.ts 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { AnyAction } from 'redux';
  2. import { IStore } from '../app/types';
  3. import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
  4. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  5. import {
  6. SET_REQUESTING_SUBTITLES,
  7. TOGGLE_REQUESTING_SUBTITLES
  8. } from './actionTypes';
  9. import {
  10. removeTranscriptMessage,
  11. updateTranscriptMessage
  12. } from './actions.any';
  13. import { notifyTranscriptionChunkReceived } from './functions';
  14. /**
  15. * The type of json-message which indicates that json carries a
  16. * transcription result.
  17. */
  18. const JSON_TYPE_TRANSCRIPTION_RESULT = 'transcription-result';
  19. /**
  20. * The type of json-message which indicates that json carries a
  21. * translation result.
  22. */
  23. const JSON_TYPE_TRANSLATION_RESULT = 'translation-result';
  24. /**
  25. * The local participant property which is used to set whether the local
  26. * participant wants to have a transcriber in the room.
  27. */
  28. const P_NAME_REQUESTING_TRANSCRIPTION = 'requestingTranscription';
  29. /**
  30. * The local participant property which is used to store the language
  31. * preference for translation for a participant.
  32. */
  33. const P_NAME_TRANSLATION_LANGUAGE = 'translation_language';
  34. /**
  35. * Time after which the rendered subtitles will be removed.
  36. */
  37. const REMOVE_AFTER_MS = 3000;
  38. /**
  39. * Stability factor for a transcription. We'll treat a transcript as stable
  40. * beyond this value.
  41. */
  42. const STABLE_TRANSCRIPTION_FACTOR = 0.85;
  43. /**
  44. * Middleware that catches actions related to transcript messages to be rendered
  45. * in {@link Captions}.
  46. *
  47. * @param {Store} store - The redux store.
  48. * @returns {Function}
  49. */
  50. MiddlewareRegistry.register(store => next => action => {
  51. switch (action.type) {
  52. case ENDPOINT_MESSAGE_RECEIVED:
  53. return _endpointMessageReceived(store, next, action);
  54. case TOGGLE_REQUESTING_SUBTITLES: {
  55. const state = store.getState()['features/subtitles'];
  56. const toggledValue = !state._requestingSubtitles;
  57. _requestingSubtitlesChange(store, toggledValue, state._language);
  58. break;
  59. }
  60. case SET_REQUESTING_SUBTITLES:
  61. _requestingSubtitlesChange(store, action.enabled, action.language);
  62. break;
  63. }
  64. return next(action);
  65. });
  66. /**
  67. * Notifies the feature transcription that the action
  68. * {@code ENDPOINT_MESSAGE_RECEIVED} is being dispatched within a specific redux
  69. * store.
  70. *
  71. * @param {Store} store - The redux store in which the specified {@code action}
  72. * is being dispatched.
  73. * @param {Dispatch} next - The redux {@code dispatch} function to
  74. * dispatch the specified {@code action} to the specified {@code store}.
  75. * @param {Action} action - The redux action {@code ENDPOINT_MESSAGE_RECEIVED}
  76. * which is being dispatched in the specified {@code store}.
  77. * @private
  78. * @returns {Object} The value returned by {@code next(action)}.
  79. */
  80. function _endpointMessageReceived(store: IStore, next: Function, action: AnyAction) {
  81. const { data: json } = action;
  82. if (![ JSON_TYPE_TRANSCRIPTION_RESULT, JSON_TYPE_TRANSLATION_RESULT ].includes(json?.type)) {
  83. return next(action);
  84. }
  85. const { dispatch, getState } = store;
  86. const state = getState();
  87. const language
  88. = state['features/base/conference'].conference
  89. ?.getLocalParticipantProperty(P_NAME_TRANSLATION_LANGUAGE);
  90. const { dumpTranscript, skipInterimTranscriptions } = state['features/base/config'].testing ?? {};
  91. const transcriptMessageID = json.message_id;
  92. const { name, id, avatar_url: avatarUrl } = json.participant;
  93. const participant = {
  94. avatarUrl,
  95. id,
  96. name
  97. };
  98. if (json.type === JSON_TYPE_TRANSLATION_RESULT && json.language === language) {
  99. // Displays final results in the target language if translation is
  100. // enabled.
  101. const newTranscriptMessage = {
  102. clearTimeOut: undefined,
  103. final: json.text,
  104. participant
  105. };
  106. _setClearerOnTranscriptMessage(dispatch, transcriptMessageID, newTranscriptMessage);
  107. dispatch(updateTranscriptMessage(transcriptMessageID, newTranscriptMessage));
  108. } else if (json.type === JSON_TYPE_TRANSCRIPTION_RESULT) {
  109. // Displays interim and final results without any translation if
  110. // translations are disabled.
  111. const { text } = json.transcript[0];
  112. // First, notify the external API.
  113. if (!(json.is_interim && skipInterimTranscriptions)) {
  114. const txt: any = {};
  115. if (!json.is_interim) {
  116. txt.final = text;
  117. } else if (json.stability > STABLE_TRANSCRIPTION_FACTOR) {
  118. txt.stable = text;
  119. } else {
  120. txt.unstable = text;
  121. }
  122. notifyTranscriptionChunkReceived(
  123. transcriptMessageID,
  124. json.language,
  125. participant,
  126. txt,
  127. store
  128. );
  129. if (navigator.product !== 'ReactNative') {
  130. // Dump transcript in a <transcript> element for debugging purposes.
  131. if (!json.is_interim && dumpTranscript) {
  132. try {
  133. let elem = document.body.getElementsByTagName('transcript')[0];
  134. // eslint-disable-next-line max-depth
  135. if (!elem) {
  136. elem = document.createElement('transcript');
  137. document.body.appendChild(elem);
  138. }
  139. elem.append(`${new Date(json.timestamp).toISOString()} ${participant.name}: ${text}`);
  140. } catch (_) {
  141. // Ignored.
  142. }
  143. }
  144. }
  145. }
  146. // If the user is not requesting transcriptions just bail.
  147. // Regex to filter out all possible country codes after language code:
  148. // this should catch all notations like 'en-GB' 'en_GB' and 'enGB'
  149. // and be independent of the country code length
  150. if (json.language.replace(/[-_A-Z].*/, '') !== language) {
  151. return next(action);
  152. }
  153. if (json.is_interim && skipInterimTranscriptions) {
  154. return next(action);
  155. }
  156. // We update the previous transcript message with the same
  157. // message ID or adds a new transcript message if it does not
  158. // exist in the map.
  159. const existingMessage = state['features/subtitles']._transcriptMessages.get(transcriptMessageID);
  160. const newTranscriptMessage: any = {
  161. clearTimeOut: existingMessage?.clearTimeOut,
  162. language,
  163. participant
  164. };
  165. _setClearerOnTranscriptMessage(dispatch, transcriptMessageID, newTranscriptMessage);
  166. // If this is final result, update the state as a final result
  167. // and start a count down to remove the subtitle from the state
  168. if (!json.is_interim) {
  169. newTranscriptMessage.final = text;
  170. } else if (json.stability > STABLE_TRANSCRIPTION_FACTOR) {
  171. // If the message has a high stability, we can update the
  172. // stable field of the state and remove the previously
  173. // unstable results
  174. newTranscriptMessage.stable = text;
  175. } else {
  176. // Otherwise, this result has an unstable result, which we
  177. // add to the state. The unstable result will be appended
  178. // after the stable part.
  179. newTranscriptMessage.unstable = text;
  180. }
  181. dispatch(updateTranscriptMessage(transcriptMessageID, newTranscriptMessage));
  182. }
  183. return next(action);
  184. }
  185. /**
  186. * Toggle the local property 'requestingTranscription'. This will cause Jicofo
  187. * and Jigasi to decide whether the transcriber needs to be in the room.
  188. *
  189. * @param {Store} store - The redux store.
  190. * @param {boolean} enabled - Whether subtitles should be enabled or not.
  191. * @param {string} language - The language to use for translation.
  192. * @private
  193. * @returns {void}
  194. */
  195. function _requestingSubtitlesChange(
  196. { getState }: IStore,
  197. enabled: boolean,
  198. language?: string | null) {
  199. const state = getState();
  200. const { conference } = state['features/base/conference'];
  201. conference?.setLocalParticipantProperty(
  202. P_NAME_REQUESTING_TRANSCRIPTION,
  203. enabled);
  204. if (enabled && language) {
  205. conference?.setLocalParticipantProperty(
  206. P_NAME_TRANSLATION_LANGUAGE,
  207. language.replace('translation-languages:', ''));
  208. }
  209. }
  210. /**
  211. * Set a timeout on a TranscriptMessage object so it clears itself when it's not
  212. * updated.
  213. *
  214. * @param {Function} dispatch - Dispatch remove action to store.
  215. * @param {string} transcriptMessageID - The id of the message to remove.
  216. * @param {Object} transcriptMessage - The message to remove.
  217. * @returns {void}
  218. */
  219. function _setClearerOnTranscriptMessage(
  220. dispatch: IStore['dispatch'],
  221. transcriptMessageID: string,
  222. transcriptMessage: { clearTimeOut?: number; }) {
  223. if (transcriptMessage.clearTimeOut) {
  224. clearTimeout(transcriptMessage.clearTimeOut);
  225. }
  226. transcriptMessage.clearTimeOut
  227. = window.setTimeout(
  228. () => dispatch(removeTranscriptMessage(transcriptMessageID)),
  229. REMOVE_AFTER_MS);
  230. }