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.

LocalRecordingInfoDialog.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. // @flow
  2. import moment from 'moment';
  3. import React, { Component } from 'react';
  4. import type { Dispatch } from 'redux';
  5. import { Dialog } from '../../base/dialog';
  6. import { translate } from '../../base/i18n';
  7. import {
  8. PARTICIPANT_ROLE,
  9. getLocalParticipant
  10. } from '../../base/participants';
  11. import { connect } from '../../base/redux';
  12. import { statsUpdate } from '../actions';
  13. import { recordingController } from '../controller';
  14. /**
  15. * The type of the React {@code Component} props of
  16. * {@link LocalRecordingInfoDialog}.
  17. */
  18. type Props = {
  19. /**
  20. * Redux store dispatch function.
  21. */
  22. dispatch: Dispatch<any>,
  23. /**
  24. * Current encoding format.
  25. */
  26. encodingFormat: string,
  27. /**
  28. * Whether the local user is the moderator.
  29. */
  30. isModerator: boolean,
  31. /**
  32. * Whether local recording is engaged.
  33. */
  34. isEngaged: boolean,
  35. /**
  36. * The start time of the current local recording session.
  37. * Used to calculate the duration of recording.
  38. */
  39. recordingEngagedAt: Date,
  40. /**
  41. * Stats of all the participant.
  42. */
  43. stats: Object,
  44. /**
  45. * Invoked to obtain translated strings.
  46. */
  47. t: Function
  48. }
  49. /**
  50. * The type of the React {@code Component} state of
  51. * {@link LocalRecordingInfoDialog}.
  52. */
  53. type State = {
  54. /**
  55. * The recording duration string to be displayed on the UI.
  56. */
  57. durationString: string
  58. }
  59. /**
  60. * A React Component with the contents for a dialog that shows information about
  61. * local recording. For users with moderator rights, this is also the "control
  62. * panel" for starting/stopping local recording on all clients.
  63. *
  64. * @extends Component
  65. */
  66. class LocalRecordingInfoDialog extends Component<Props, State> {
  67. /**
  68. * Saves a handle to the timer for UI updates,
  69. * so that it can be cancelled when the component unmounts.
  70. */
  71. _timer: ?IntervalID;
  72. /**
  73. * Initializes a new {@code LocalRecordingInfoDialog} instance.
  74. *
  75. * @param {Props} props - The React {@code Component} props to initialize
  76. * the new {@code LocalRecordingInfoDialog} instance with.
  77. */
  78. constructor(props: Props) {
  79. super(props);
  80. this.state = {
  81. durationString: ''
  82. };
  83. }
  84. /**
  85. * Implements React's {@link Component#componentDidMount()}.
  86. *
  87. * @returns {void}
  88. */
  89. componentDidMount() {
  90. this._timer = setInterval(
  91. () => {
  92. this.setState((_prevState, props) => {
  93. const nowTime = new Date();
  94. return {
  95. durationString: this._getDuration(nowTime,
  96. props.recordingEngagedAt)
  97. };
  98. });
  99. try {
  100. this.props.dispatch(
  101. statsUpdate(recordingController
  102. .getParticipantsStats()));
  103. } catch (e) {
  104. // do nothing
  105. }
  106. },
  107. 1000
  108. );
  109. }
  110. /**
  111. * Implements React's {@link Component#componentWillUnmount()}.
  112. *
  113. * @returns {void}
  114. */
  115. componentWillUnmount() {
  116. if (this._timer) {
  117. clearInterval(this._timer);
  118. this._timer = null;
  119. }
  120. }
  121. /**
  122. * Implements React's {@link Component#render()}.
  123. *
  124. * @inheritdoc
  125. * @returns {ReactElement}
  126. */
  127. render() {
  128. const { isModerator, t } = this.props;
  129. return (
  130. <Dialog
  131. cancelKey = { 'dialog.close' }
  132. submitDisabled = { true }
  133. titleKey = 'localRecording.dialogTitle'>
  134. <div className = 'localrec-control'>
  135. <span className = 'localrec-control-info-label'>
  136. {`${t('localRecording.moderator')}:`}
  137. </span>
  138. <span className = 'info-value'>
  139. { isModerator
  140. ? t('localRecording.yes')
  141. : t('localRecording.no') }
  142. </span>
  143. </div>
  144. { this._renderModeratorControls() }
  145. { this._renderDurationAndFormat() }
  146. </Dialog>
  147. );
  148. }
  149. /**
  150. * Renders the recording duration and encoding format. Only shown if local
  151. * recording is engaged.
  152. *
  153. * @private
  154. * @returns {ReactElement|null}
  155. */
  156. _renderDurationAndFormat() {
  157. const { encodingFormat, isEngaged, t } = this.props;
  158. const { durationString } = this.state;
  159. if (!isEngaged) {
  160. return null;
  161. }
  162. return (
  163. <div>
  164. <div>
  165. <span className = 'localrec-control-info-label'>
  166. {`${t('localRecording.duration')}:`}
  167. </span>
  168. <span className = 'info-value'>
  169. { durationString === ''
  170. ? t('localRecording.durationNA')
  171. : durationString }
  172. </span>
  173. </div>
  174. <div>
  175. <span className = 'localrec-control-info-label'>
  176. {`${t('localRecording.encoding')}:`}
  177. </span>
  178. <span className = 'info-value'>
  179. { encodingFormat }
  180. </span>
  181. </div>
  182. </div>
  183. );
  184. }
  185. /**
  186. * Returns React elements for displaying the local recording stats of
  187. * each participant.
  188. *
  189. * @private
  190. * @returns {ReactElement|null}
  191. */
  192. _renderStats() {
  193. const { stats } = this.props;
  194. if (stats === undefined) {
  195. return null;
  196. }
  197. const ids = Object.keys(stats);
  198. return (
  199. <div className = 'localrec-participant-stats' >
  200. { this._renderStatsHeader() }
  201. { ids.map((id, i) => this._renderStatsLine(i, id)) }
  202. </div>
  203. );
  204. }
  205. /**
  206. * Renders the stats for one participant.
  207. *
  208. * @private
  209. * @param {*} lineKey - The key required by React for elements in lists.
  210. * @param {*} id - The ID of the participant.
  211. * @returns {ReactElement}
  212. */
  213. _renderStatsLine(lineKey, id) {
  214. const { stats } = this.props;
  215. let statusClass = 'localrec-participant-stats-item__status-dot ';
  216. statusClass += stats[id].recordingStats
  217. ? stats[id].recordingStats.isRecording
  218. ? 'status-on'
  219. : 'status-off'
  220. : 'status-unknown';
  221. return (
  222. <div
  223. className = 'localrec-participant-stats-item'
  224. key = { lineKey } >
  225. <div className = 'localrec-participant-stats-item__status'>
  226. <span className = { statusClass } />
  227. </div>
  228. <div className = 'localrec-participant-stats-item__name'>
  229. { stats[id].displayName || id }
  230. </div>
  231. <div className = 'localrec-participant-stats-item__sessionid'>
  232. { stats[id].recordingStats.currentSessionToken }
  233. </div>
  234. </div>
  235. );
  236. }
  237. /**
  238. * Renders the participant stats header line.
  239. *
  240. * @private
  241. * @returns {ReactElement}
  242. */
  243. _renderStatsHeader() {
  244. const { t } = this.props;
  245. return (
  246. <div className = 'localrec-participant-stats-item'>
  247. <div className = 'localrec-participant-stats-item__status' />
  248. <div className = 'localrec-participant-stats-item__name'>
  249. { t('localRecording.participant') }
  250. </div>
  251. <div className = 'localrec-participant-stats-item__sessionid'>
  252. { t('localRecording.sessionToken') }
  253. </div>
  254. </div>
  255. );
  256. }
  257. /**
  258. * Renders the moderator-only controls: The stats of all users and the
  259. * action links.
  260. *
  261. * @private
  262. * @returns {ReactElement|null}
  263. */
  264. _renderModeratorControls() {
  265. const { isModerator, isEngaged, t } = this.props;
  266. if (!isModerator) {
  267. return null;
  268. }
  269. return (
  270. <div>
  271. <div className = 'localrec-control-action-links'>
  272. <div className = 'localrec-control-action-link'>
  273. { isEngaged ? <a
  274. onClick = { this._onStop }>
  275. { t('localRecording.stop') }
  276. </a>
  277. : <a
  278. onClick = { this._onStart }>
  279. { t('localRecording.start') }
  280. </a>
  281. }
  282. </div>
  283. </div>
  284. <div>
  285. <span className = 'localrec-control-info-label'>
  286. {`${t('localRecording.participantStats')}:`}
  287. </span>
  288. </div>
  289. { this._renderStats() }
  290. </div>
  291. );
  292. }
  293. /**
  294. * Creates a duration string "HH:MM:SS" from two Date objects.
  295. *
  296. * @param {Date} now - Current time.
  297. * @param {Date} prev - Previous time, the time to be subtracted.
  298. * @returns {string}
  299. */
  300. _getDuration(now, prev) {
  301. if (prev === null || prev === undefined) {
  302. return '';
  303. }
  304. // Still a hack, as moment.js does not support formatting of duration
  305. // (i.e. TimeDelta). Only works if total duration < 24 hours.
  306. // But who is going to have a 24-hour long conference?
  307. return moment(now - prev).utc()
  308. .format('HH:mm:ss');
  309. }
  310. /**
  311. * Callback function for the Start UI action.
  312. *
  313. * @private
  314. * @returns {void}
  315. */
  316. _onStart() {
  317. recordingController.startRecording();
  318. }
  319. /**
  320. * Callback function for the Stop UI action.
  321. *
  322. * @private
  323. * @returns {void}
  324. */
  325. _onStop() {
  326. recordingController.stopRecording();
  327. }
  328. }
  329. /**
  330. * Maps (parts of) the Redux state to the associated props for the
  331. * {@code LocalRecordingInfoDialog} component.
  332. *
  333. * @param {Object} state - The Redux state.
  334. * @private
  335. * @returns {{
  336. * encodingFormat: string,
  337. * isModerator: boolean,
  338. * isEngaged: boolean,
  339. * recordingEngagedAt: Date,
  340. * stats: Object
  341. * }}
  342. */
  343. function _mapStateToProps(state) {
  344. const {
  345. encodingFormat,
  346. isEngaged,
  347. recordingEngagedAt,
  348. stats
  349. } = state['features/local-recording'];
  350. const isModerator
  351. = getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR;
  352. return {
  353. encodingFormat,
  354. isModerator,
  355. isEngaged,
  356. recordingEngagedAt,
  357. stats
  358. };
  359. }
  360. export default translate(connect(_mapStateToProps)(LocalRecordingInfoDialog));