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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. /* global APP, $, config, interfaceConfig */
  2. /*
  3. * Copyright @ 2015 Atlassian Pty Ltd
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. const logger = require('jitsi-meet-logger').getLogger(__filename);
  18. import UIEvents from '../../../service/UI/UIEvents';
  19. import UIUtil from '../util/UIUtil';
  20. import VideoLayout from '../videolayout/VideoLayout';
  21. import { openDialog } from '../../../react/features/base/dialog';
  22. import {
  23. JitsiRecordingStatus
  24. } from '../../../react/features/base/lib-jitsi-meet';
  25. import {
  26. createToolbarEvent,
  27. createRecordingDialogEvent,
  28. sendAnalytics
  29. } from '../../../react/features/analytics';
  30. import { setToolboxEnabled } from '../../../react/features/toolbox';
  31. import { setNotificationsEnabled } from '../../../react/features/notifications';
  32. import {
  33. StartLiveStreamDialog,
  34. StopLiveStreamDialog,
  35. hideRecordingLabel,
  36. setRecordingType,
  37. updateRecordingState
  38. } from '../../../react/features/recording';
  39. /**
  40. * Translation keys to use for display in the UI when recording the conference
  41. * but not streaming live.
  42. *
  43. * @private
  44. * @type {Object}
  45. */
  46. export const RECORDING_TRANSLATION_KEYS = {
  47. failedToStartKey: 'recording.failedToStart',
  48. recordingBusy: 'recording.busy',
  49. recordingBusyTitle: 'recording.busyTitle',
  50. recordingButtonTooltip: 'recording.buttonTooltip',
  51. recordingErrorKey: 'recording.error',
  52. recordingOffKey: 'recording.off',
  53. recordingOnKey: 'recording.on',
  54. recordingPendingKey: 'recording.pending',
  55. recordingTitle: 'dialog.recording',
  56. recordingUnavailable: 'recording.unavailable',
  57. recordingUnavailableParams: '$t(recording.serviceName)',
  58. recordingUnavailableTitle: 'recording.unavailableTitle'
  59. };
  60. /**
  61. * Translation keys to use for display in the UI when the recording mode is
  62. * currently streaming live.
  63. *
  64. * @private
  65. * @type {Object}
  66. */
  67. export const STREAMING_TRANSLATION_KEYS = {
  68. failedToStartKey: 'liveStreaming.failedToStart',
  69. recordingBusy: 'liveStreaming.busy',
  70. recordingBusyTitle: 'liveStreaming.busyTitle',
  71. recordingButtonTooltip: 'liveStreaming.buttonTooltip',
  72. recordingErrorKey: 'liveStreaming.error',
  73. recordingOffKey: 'liveStreaming.off',
  74. recordingOnKey: 'liveStreaming.on',
  75. recordingPendingKey: 'liveStreaming.pending',
  76. recordingTitle: 'dialog.liveStreaming',
  77. recordingUnavailable: 'recording.unavailable',
  78. recordingUnavailableParams: '$t(liveStreaming.serviceName)',
  79. recordingUnavailableTitle: 'liveStreaming.unavailableTitle'
  80. };
  81. /**
  82. * The dialog for user input.
  83. */
  84. let dialog = null;
  85. /**
  86. * Indicates if the recording button should be enabled.
  87. *
  88. * @returns {boolean} {true} if the
  89. * @private
  90. */
  91. function _isRecordingButtonEnabled() {
  92. return (
  93. interfaceConfig.TOOLBAR_BUTTONS.indexOf('recording') !== -1
  94. && config.enableRecording
  95. && APP.conference.isRecordingSupported());
  96. }
  97. /**
  98. * Request live stream token from the user.
  99. * @returns {Promise}
  100. */
  101. function _requestLiveStreamId() {
  102. return new Promise((resolve, reject) =>
  103. APP.store.dispatch(openDialog(StartLiveStreamDialog, {
  104. onCancel: reject,
  105. onSubmit: resolve
  106. })));
  107. }
  108. /**
  109. * Request recording token from the user.
  110. * @returns {Promise}
  111. */
  112. function _requestRecordingToken() {
  113. const titleKey = 'dialog.recordingToken';
  114. const msgString
  115. = `<input name="recordingToken" type="text"
  116. data-i18n="[placeholder]dialog.token"
  117. class="input-control"
  118. autofocus>`
  119. ;
  120. return new Promise((resolve, reject) => {
  121. dialog = APP.UI.messageHandler.openTwoButtonDialog({
  122. titleKey,
  123. msgString,
  124. leftButtonKey: 'dialog.Save',
  125. submitFunction(e, v, m, f) { // eslint-disable-line max-params
  126. if (v && f.recordingToken) {
  127. resolve(UIUtil.escapeHtml(f.recordingToken));
  128. } else {
  129. reject(APP.UI.messageHandler.CANCEL);
  130. }
  131. },
  132. closeFunction() {
  133. dialog = null;
  134. },
  135. focus: ':input:first'
  136. });
  137. });
  138. }
  139. /**
  140. * Shows a prompt dialog to the user when they have toggled off the recording.
  141. *
  142. * @param recordingType the recording type
  143. * @returns {Promise}
  144. * @private
  145. */
  146. function _showStopRecordingPrompt(recordingType) {
  147. if (recordingType === 'jibri') {
  148. return new Promise((resolve, reject) => {
  149. APP.store.dispatch(openDialog(StopLiveStreamDialog, {
  150. onCancel: reject,
  151. onSubmit: resolve
  152. }));
  153. });
  154. }
  155. return new Promise((resolve, reject) => {
  156. dialog = APP.UI.messageHandler.openTwoButtonDialog({
  157. titleKey: 'dialog.recording',
  158. msgKey: 'dialog.stopRecordingWarning',
  159. leftButtonKey: 'dialog.stopRecording',
  160. submitFunction: (e, v) => (v ? resolve : reject)(),
  161. closeFunction: () => {
  162. dialog = null;
  163. }
  164. });
  165. });
  166. }
  167. /**
  168. * Checks whether if the given status is either PENDING or RETRYING
  169. * @param status {JitsiRecordingStatus} Jibri status to be checked
  170. * @returns {boolean} true if the condition is met or false otherwise.
  171. */
  172. function isStartingStatus(status) {
  173. return (
  174. status === JitsiRecordingStatus.PENDING
  175. || status === JitsiRecordingStatus.RETRYING
  176. );
  177. }
  178. /**
  179. * Manages the recording user interface and user experience.
  180. * @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
  181. * updateRecordingUI, checkAutoRecord}}
  182. */
  183. const Recording = {
  184. /**
  185. * Initializes the recording UI.
  186. */
  187. init(eventEmitter, recordingType) {
  188. this.eventEmitter = eventEmitter;
  189. this.recordingType = recordingType;
  190. APP.store.dispatch(setRecordingType(recordingType));
  191. this.updateRecordingState(APP.conference.getRecordingState());
  192. if (recordingType === 'jibri') {
  193. this.baseClass = 'fa fa-play-circle';
  194. Object.assign(this, STREAMING_TRANSLATION_KEYS);
  195. } else {
  196. this.baseClass = 'icon-recEnable';
  197. Object.assign(this, RECORDING_TRANSLATION_KEYS);
  198. }
  199. // XXX Due to the React-ification of Toolbox, the HTMLElement with id
  200. // toolbar_button_record may not exist yet.
  201. $(document).on(
  202. 'click',
  203. '#toolbar_button_record',
  204. ev => this._onToolbarButtonClick(ev));
  205. this.eventEmitter.on(UIEvents.TOGGLE_RECORDING,
  206. () => this._onToolbarButtonClick());
  207. // If I am a recorder then I publish my recorder custom role to notify
  208. // everyone.
  209. if (config.iAmRecorder) {
  210. VideoLayout.enableDeviceAvailabilityIcons(
  211. APP.conference.getMyUserId(), false);
  212. // in case of iAmSipGateway keep local video visible
  213. if (!config.iAmSipGateway) {
  214. VideoLayout.setLocalVideoVisible(false);
  215. }
  216. APP.store.dispatch(setToolboxEnabled(false));
  217. APP.store.dispatch(setNotificationsEnabled(false));
  218. APP.UI.messageHandler.enablePopups(false);
  219. }
  220. },
  221. /**
  222. * Initialise the recording button.
  223. */
  224. initRecordingButton() {
  225. const selector = $('#toolbar_button_record');
  226. selector.addClass(this.baseClass);
  227. selector.attr('data-i18n', `[content]${this.recordingButtonTooltip}`);
  228. APP.translation.translateElement(selector);
  229. },
  230. /**
  231. * Shows or hides the 'recording' button.
  232. * @param show {true} to show the recording button, {false} to hide it
  233. */
  234. showRecordingButton(show) {
  235. const shouldShow = show && _isRecordingButtonEnabled();
  236. const id = 'toolbar_button_record';
  237. UIUtil.setVisible(id, shouldShow);
  238. },
  239. /**
  240. * Updates the recording state UI.
  241. * @param recordingState gives us the current recording state
  242. */
  243. updateRecordingState(recordingState) {
  244. // I'm the recorder, so I don't want to see any UI related to states.
  245. if (config.iAmRecorder) {
  246. return;
  247. }
  248. // If there's no state change, we ignore the update.
  249. if (!recordingState || this.currentState === recordingState) {
  250. return;
  251. }
  252. this.updateRecordingUI(recordingState);
  253. },
  254. /**
  255. * Sets the state of the recording button.
  256. * @param recordingState gives us the current recording state
  257. */
  258. updateRecordingUI(recordingState) {
  259. const oldState = this.currentState;
  260. this.currentState = recordingState;
  261. let labelDisplayConfiguration;
  262. let isRecording = false;
  263. switch (recordingState) {
  264. case JitsiRecordingStatus.ON:
  265. case JitsiRecordingStatus.RETRYING: {
  266. labelDisplayConfiguration = {
  267. centered: false,
  268. key: this.recordingOnKey,
  269. showSpinner: recordingState === JitsiRecordingStatus.RETRYING
  270. };
  271. this._setToolbarButtonToggled(true);
  272. isRecording = true;
  273. break;
  274. }
  275. case JitsiRecordingStatus.OFF:
  276. case JitsiRecordingStatus.BUSY:
  277. case JitsiRecordingStatus.FAILED:
  278. case JitsiRecordingStatus.UNAVAILABLE: {
  279. const wasInStartingStatus = isStartingStatus(oldState);
  280. // We don't want UI changes if this is an availability change.
  281. if (oldState !== JitsiRecordingStatus.ON && !wasInStartingStatus) {
  282. APP.store.dispatch(updateRecordingState({ recordingState }));
  283. return;
  284. }
  285. labelDisplayConfiguration = {
  286. centered: true,
  287. key: wasInStartingStatus
  288. ? this.failedToStartKey
  289. : this.recordingOffKey
  290. };
  291. this._setToolbarButtonToggled(false);
  292. setTimeout(() => {
  293. APP.store.dispatch(hideRecordingLabel());
  294. }, 5000);
  295. break;
  296. }
  297. case JitsiRecordingStatus.PENDING: {
  298. labelDisplayConfiguration = {
  299. centered: true,
  300. key: this.recordingPendingKey
  301. };
  302. this._setToolbarButtonToggled(false);
  303. break;
  304. }
  305. case JitsiRecordingStatus.ERROR: {
  306. labelDisplayConfiguration = {
  307. centered: true,
  308. key: this.recordingErrorKey
  309. };
  310. this._setToolbarButtonToggled(false);
  311. break;
  312. }
  313. // Return an empty label display configuration to indicate no label
  314. // should be displayed. The JitsiRecordingStatus.AVAIABLE case is
  315. // handled here.
  316. default: {
  317. labelDisplayConfiguration = null;
  318. }
  319. }
  320. APP.store.dispatch(updateRecordingState({
  321. isRecording,
  322. labelDisplayConfiguration,
  323. recordingState
  324. }));
  325. },
  326. // checks whether recording is enabled and whether we have params
  327. // to start automatically recording (XXX: No, it doesn't do that).
  328. checkAutoRecord() {
  329. if (_isRecordingButtonEnabled && config.autoRecord) {
  330. this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
  331. this.eventEmitter.emit(
  332. UIEvents.RECORDING_TOGGLED,
  333. { token: this.predefinedToken });
  334. }
  335. },
  336. /**
  337. * Handles {@code click} on {@code toolbar_button_record}.
  338. *
  339. * @returns {void}
  340. */
  341. _onToolbarButtonClick() {
  342. sendAnalytics(createToolbarEvent(
  343. 'recording.button',
  344. {
  345. 'dialog_present': Boolean(dialog)
  346. }));
  347. if (dialog) {
  348. return;
  349. }
  350. switch (this.currentState) {
  351. case JitsiRecordingStatus.ON:
  352. case JitsiRecordingStatus.RETRYING:
  353. case JitsiRecordingStatus.PENDING: {
  354. _showStopRecordingPrompt(this.recordingType).then(
  355. () => {
  356. this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
  357. // The confirm button on the stop recording dialog was
  358. // clicked
  359. sendAnalytics(
  360. createRecordingDialogEvent(
  361. 'stop',
  362. 'confirm.button'));
  363. },
  364. () => {}); // eslint-disable-line no-empty-function
  365. break;
  366. }
  367. case JitsiRecordingStatus.AVAILABLE:
  368. case JitsiRecordingStatus.OFF: {
  369. if (this.recordingType === 'jibri') {
  370. _requestLiveStreamId()
  371. .then(streamId => {
  372. this.eventEmitter.emit(
  373. UIEvents.RECORDING_TOGGLED,
  374. { streamId });
  375. // The confirm button on the start recording dialog was
  376. // clicked
  377. sendAnalytics(
  378. createRecordingDialogEvent(
  379. 'start',
  380. 'confirm.button'));
  381. })
  382. .catch(reason => {
  383. if (reason === APP.UI.messageHandler.CANCEL) {
  384. // The cancel button on the start recording dialog was
  385. // clicked
  386. sendAnalytics(
  387. createRecordingDialogEvent(
  388. 'start',
  389. 'cancel.button'));
  390. } else {
  391. logger.error(reason);
  392. }
  393. });
  394. } else {
  395. // Note that we only fire analytics events for Jibri.
  396. if (this.predefinedToken) {
  397. this.eventEmitter.emit(
  398. UIEvents.RECORDING_TOGGLED,
  399. { token: this.predefinedToken });
  400. return;
  401. }
  402. _requestRecordingToken().then(token => {
  403. this.eventEmitter.emit(
  404. UIEvents.RECORDING_TOGGLED,
  405. { token });
  406. })
  407. .catch(reason => {
  408. if (reason !== APP.UI.messageHandler.CANCEL) {
  409. logger.error(reason);
  410. }
  411. });
  412. }
  413. break;
  414. }
  415. case JitsiRecordingStatus.BUSY: {
  416. APP.UI.messageHandler.showWarning({
  417. descriptionKey: this.recordingBusy,
  418. titleKey: this.recordingBusyTitle
  419. });
  420. break;
  421. }
  422. default: {
  423. APP.UI.messageHandler.showError({
  424. descriptionKey: this.recordingUnavailable,
  425. descriptionArguments: {
  426. serviceName: this.recordingUnavailableParams },
  427. titleKey: this.recordingUnavailableTitle
  428. });
  429. }
  430. }
  431. },
  432. /**
  433. * Sets the toggled state of the recording toolbar button.
  434. *
  435. * @param {boolean} isToggled indicates if the button should be toggled
  436. * or not
  437. */
  438. _setToolbarButtonToggled(isToggled) {
  439. $('#toolbar_button_record').toggleClass('toggled', isToggled);
  440. }
  441. };
  442. export default Recording;