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

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