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.

Recording.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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 {
  22. JitsiRecordingStatus
  23. } from '../../../react/features/base/lib-jitsi-meet';
  24. import {
  25. sendAnalyticsEvent
  26. } from '../../../react/features/analytics';
  27. import { setToolboxEnabled } from '../../../react/features/toolbox';
  28. import { setNotificationsEnabled } from '../../../react/features/notifications';
  29. import {
  30. hideRecordingLabel,
  31. updateRecordingState
  32. } from '../../../react/features/recording';
  33. /**
  34. * Translation keys to use for display in the UI when recording the conference
  35. * but not streaming live.
  36. *
  37. * @private
  38. * @type {Object}
  39. */
  40. export const RECORDING_TRANSLATION_KEYS = {
  41. failedToStartKey: 'recording.failedToStart',
  42. recordingBusy: 'recording.busy',
  43. recordingBusyTitle: 'recording.busyTitle',
  44. recordingButtonTooltip: 'recording.buttonTooltip',
  45. recordingErrorKey: 'recording.error',
  46. recordingOffKey: 'recording.off',
  47. recordingOnKey: 'recording.on',
  48. recordingPendingKey: 'recording.pending',
  49. recordingTitle: 'dialog.recording',
  50. recordingUnavailable: 'recording.unavailable',
  51. recordingUnavailableTitle: 'recording.unavailableTitle'
  52. };
  53. /**
  54. * Translation keys to use for display in the UI when the recording mode is
  55. * currently streaming live.
  56. *
  57. * @private
  58. * @type {Object}
  59. */
  60. export const STREAMING_TRANSLATION_KEYS = {
  61. failedToStartKey: 'liveStreaming.failedToStart',
  62. recordingBusy: 'liveStreaming.busy',
  63. recordingBusyTitle: 'liveStreaming.busyTitle',
  64. recordingButtonTooltip: 'liveStreaming.buttonTooltip',
  65. recordingErrorKey: 'liveStreaming.error',
  66. recordingOffKey: 'liveStreaming.off',
  67. recordingOnKey: 'liveStreaming.on',
  68. recordingPendingKey: 'liveStreaming.pending',
  69. recordingTitle: 'dialog.liveStreaming',
  70. recordingUnavailable: 'liveStreaming.unavailable',
  71. recordingUnavailableTitle: 'liveStreaming.unavailableTitle'
  72. };
  73. /**
  74. * The dialog for user input.
  75. */
  76. let dialog = null;
  77. /**
  78. * Indicates if the recording button should be enabled.
  79. *
  80. * @returns {boolean} {true} if the
  81. * @private
  82. */
  83. function _isRecordingButtonEnabled() {
  84. return (
  85. interfaceConfig.TOOLBAR_BUTTONS.indexOf('recording') !== -1
  86. && config.enableRecording
  87. && APP.conference.isRecordingSupported());
  88. }
  89. /**
  90. * Request live stream token from the user.
  91. * @returns {Promise}
  92. */
  93. function _requestLiveStreamId() {
  94. const cancelButton
  95. = APP.translation.generateTranslationHTML('dialog.Cancel');
  96. const backButton = APP.translation.generateTranslationHTML('dialog.Back');
  97. const startStreamingButton
  98. = APP.translation.generateTranslationHTML('dialog.startLiveStreaming');
  99. const streamIdRequired
  100. = APP.translation.generateTranslationHTML(
  101. 'liveStreaming.streamIdRequired');
  102. const streamIdHelp
  103. = APP.translation.generateTranslationHTML(
  104. 'liveStreaming.streamIdHelp');
  105. return new Promise((resolve, reject) => {
  106. dialog = APP.UI.messageHandler.openDialogWithStates({
  107. state0: {
  108. titleKey: 'dialog.liveStreaming',
  109. html:
  110. `<input class="input-control"
  111. name="streamId" type="text"
  112. data-i18n="[placeholder]dialog.streamKey"
  113. autofocus><div style="text-align: right">
  114. <a class="helper-link" target="_new"
  115. href="${interfaceConfig.LIVE_STREAMING_HELP_LINK}">${
  116. streamIdHelp
  117. }</a></div>`,
  118. persistent: false,
  119. buttons: [
  120. { title: cancelButton,
  121. value: false },
  122. { title: startStreamingButton,
  123. value: true }
  124. ],
  125. focus: ':input:first',
  126. defaultButton: 1,
  127. submit(e, v, m, f) { // eslint-disable-line max-params
  128. e.preventDefault();
  129. if (v) {
  130. if (f.streamId && f.streamId.length > 0) {
  131. resolve(UIUtil.escapeHtml(f.streamId));
  132. dialog.close();
  133. return;
  134. }
  135. dialog.goToState('state1');
  136. return false;
  137. }
  138. reject(APP.UI.messageHandler.CANCEL);
  139. dialog.close();
  140. return false;
  141. }
  142. },
  143. state1: {
  144. titleKey: 'dialog.liveStreaming',
  145. html: streamIdRequired,
  146. persistent: false,
  147. buttons: [
  148. { title: cancelButton,
  149. value: false },
  150. { title: backButton,
  151. value: true }
  152. ],
  153. focus: ':input:first',
  154. defaultButton: 1,
  155. submit(e, v) {
  156. e.preventDefault();
  157. if (v === 0) {
  158. reject(APP.UI.messageHandler.CANCEL);
  159. dialog.close();
  160. } else {
  161. dialog.goToState('state0');
  162. }
  163. }
  164. }
  165. }, {
  166. close() {
  167. dialog = null;
  168. }
  169. });
  170. });
  171. }
  172. /**
  173. * Request recording token from the user.
  174. * @returns {Promise}
  175. */
  176. function _requestRecordingToken() {
  177. const titleKey = 'dialog.recordingToken';
  178. const msgString
  179. = `<input name="recordingToken" type="text"
  180. data-i18n="[placeholder]dialog.token"
  181. class="input-control"
  182. autofocus>`
  183. ;
  184. return new Promise((resolve, reject) => {
  185. dialog = APP.UI.messageHandler.openTwoButtonDialog({
  186. titleKey,
  187. msgString,
  188. leftButtonKey: 'dialog.Save',
  189. submitFunction(e, v, m, f) { // eslint-disable-line max-params
  190. if (v && f.recordingToken) {
  191. resolve(UIUtil.escapeHtml(f.recordingToken));
  192. } else {
  193. reject(APP.UI.messageHandler.CANCEL);
  194. }
  195. },
  196. closeFunction() {
  197. dialog = null;
  198. },
  199. focus: ':input:first'
  200. });
  201. });
  202. }
  203. /**
  204. * Shows a prompt dialog to the user when they have toggled off the recording.
  205. *
  206. * @param recordingType the recording type
  207. * @returns {Promise}
  208. * @private
  209. */
  210. function _showStopRecordingPrompt(recordingType) {
  211. let title;
  212. let message;
  213. let buttonKey;
  214. if (recordingType === 'jibri') {
  215. title = 'dialog.liveStreaming';
  216. message = 'dialog.stopStreamingWarning';
  217. buttonKey = 'dialog.stopLiveStreaming';
  218. } else {
  219. title = 'dialog.recording';
  220. message = 'dialog.stopRecordingWarning';
  221. buttonKey = 'dialog.stopRecording';
  222. }
  223. return new Promise((resolve, reject) => {
  224. dialog = APP.UI.messageHandler.openTwoButtonDialog({
  225. titleKey: title,
  226. msgKey: message,
  227. leftButtonKey: buttonKey,
  228. submitFunction: (e, v) => (v ? resolve : reject)(),
  229. closeFunction: () => {
  230. dialog = null;
  231. }
  232. });
  233. });
  234. }
  235. /**
  236. * Checks whether if the given status is either PENDING or RETRYING
  237. * @param status {JitsiRecordingStatus} Jibri status to be checked
  238. * @returns {boolean} true if the condition is met or false otherwise.
  239. */
  240. function isStartingStatus(status) {
  241. return (
  242. status === JitsiRecordingStatus.PENDING
  243. || status === JitsiRecordingStatus.RETRYING
  244. );
  245. }
  246. /**
  247. * Manages the recording user interface and user experience.
  248. * @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
  249. * updateRecordingUI, checkAutoRecord}}
  250. */
  251. const Recording = {
  252. /**
  253. * Initializes the recording UI.
  254. */
  255. init(eventEmitter, recordingType) {
  256. this.eventEmitter = eventEmitter;
  257. this.recordingType = recordingType;
  258. this.updateRecordingState(APP.conference.getRecordingState());
  259. if (recordingType === 'jibri') {
  260. this.baseClass = 'fa fa-play-circle';
  261. Object.assign(this, STREAMING_TRANSLATION_KEYS);
  262. } else {
  263. this.baseClass = 'icon-recEnable';
  264. Object.assign(this, RECORDING_TRANSLATION_KEYS);
  265. }
  266. // XXX Due to the React-ification of Toolbox, the HTMLElement with id
  267. // toolbar_button_record may not exist yet.
  268. $(document).on(
  269. 'click',
  270. '#toolbar_button_record',
  271. ev => this._onToolbarButtonClick(ev));
  272. // If I am a recorder then I publish my recorder custom role to notify
  273. // everyone.
  274. if (config.iAmRecorder) {
  275. VideoLayout.enableDeviceAvailabilityIcons(
  276. APP.conference.getMyUserId(), false);
  277. // in case of iAmSipGateway keep local video visible
  278. if (!config.iAmSipGateway) {
  279. VideoLayout.setLocalVideoVisible(false);
  280. }
  281. APP.store.dispatch(setToolboxEnabled(false));
  282. APP.store.dispatch(setNotificationsEnabled(false));
  283. APP.UI.messageHandler.enablePopups(false);
  284. }
  285. },
  286. /**
  287. * Initialise the recording button.
  288. */
  289. initRecordingButton() {
  290. const selector = $('#toolbar_button_record');
  291. selector.addClass(this.baseClass);
  292. selector.attr('data-i18n', `[content]${this.recordingButtonTooltip}`);
  293. APP.translation.translateElement(selector);
  294. },
  295. /**
  296. * Shows or hides the 'recording' button.
  297. * @param show {true} to show the recording button, {false} to hide it
  298. */
  299. showRecordingButton(show) {
  300. const shouldShow = show && _isRecordingButtonEnabled();
  301. const id = 'toolbar_button_record';
  302. UIUtil.setVisible(id, shouldShow);
  303. },
  304. /**
  305. * Updates the recording state UI.
  306. * @param recordingState gives us the current recording state
  307. */
  308. updateRecordingState(recordingState) {
  309. // I'm the recorder, so I don't want to see any UI related to states.
  310. if (config.iAmRecorder) {
  311. return;
  312. }
  313. // If there's no state change, we ignore the update.
  314. if (!recordingState || this.currentState === recordingState) {
  315. return;
  316. }
  317. this.updateRecordingUI(recordingState);
  318. },
  319. /**
  320. * Sets the state of the recording button.
  321. * @param recordingState gives us the current recording state
  322. */
  323. updateRecordingUI(recordingState) {
  324. const oldState = this.currentState;
  325. this.currentState = recordingState;
  326. let labelDisplayConfiguration;
  327. switch (recordingState) {
  328. case JitsiRecordingStatus.ON:
  329. case JitsiRecordingStatus.RETRYING: {
  330. labelDisplayConfiguration = {
  331. centered: false,
  332. key: this.recordingOnKey,
  333. showSpinner: recordingState === JitsiRecordingStatus.RETRYING
  334. };
  335. this._setToolbarButtonToggled(true);
  336. break;
  337. }
  338. case JitsiRecordingStatus.OFF:
  339. case JitsiRecordingStatus.BUSY:
  340. case JitsiRecordingStatus.FAILED:
  341. case JitsiRecordingStatus.UNAVAILABLE: {
  342. const wasInStartingStatus = isStartingStatus(oldState);
  343. // We don't want UI changes if this is an availability change.
  344. if (oldState !== JitsiRecordingStatus.ON && !wasInStartingStatus) {
  345. APP.store.dispatch(updateRecordingState({ recordingState }));
  346. return;
  347. }
  348. labelDisplayConfiguration = {
  349. centered: true,
  350. key: wasInStartingStatus
  351. ? this.failedToStartKey
  352. : this.recordingOffKey
  353. };
  354. this._setToolbarButtonToggled(false);
  355. setTimeout(() => {
  356. APP.store.dispatch(hideRecordingLabel());
  357. }, 5000);
  358. break;
  359. }
  360. case JitsiRecordingStatus.PENDING: {
  361. labelDisplayConfiguration = {
  362. centered: true,
  363. key: this.recordingPendingKey
  364. };
  365. this._setToolbarButtonToggled(false);
  366. break;
  367. }
  368. case JitsiRecordingStatus.ERROR: {
  369. labelDisplayConfiguration = {
  370. centered: true,
  371. key: this.recordingErrorKey
  372. };
  373. this._setToolbarButtonToggled(false);
  374. break;
  375. }
  376. // Return an empty label display configuration to indicate no label
  377. // should be displayed. The JitsiRecordingStatus.AVAIABLE case is
  378. // handled here.
  379. default: {
  380. labelDisplayConfiguration = null;
  381. }
  382. }
  383. APP.store.dispatch(updateRecordingState({
  384. labelDisplayConfiguration,
  385. recordingState
  386. }));
  387. },
  388. // checks whether recording is enabled and whether we have params
  389. // to start automatically recording
  390. checkAutoRecord() {
  391. if (_isRecordingButtonEnabled && config.autoRecord) {
  392. this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
  393. this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED,
  394. this.predefinedToken);
  395. }
  396. },
  397. /**
  398. * Handles {@code click} on {@code toolbar_button_record}.
  399. *
  400. * @returns {void}
  401. */
  402. _onToolbarButtonClick() {
  403. if (dialog) {
  404. return;
  405. }
  406. sendAnalyticsEvent('recording.clicked');
  407. switch (this.currentState) {
  408. case JitsiRecordingStatus.ON:
  409. case JitsiRecordingStatus.RETRYING:
  410. case JitsiRecordingStatus.PENDING: {
  411. _showStopRecordingPrompt(this.recordingType).then(
  412. () => {
  413. this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
  414. sendAnalyticsEvent('recording.stopped');
  415. },
  416. () => {}); // eslint-disable-line no-empty-function
  417. break;
  418. }
  419. case JitsiRecordingStatus.AVAILABLE:
  420. case JitsiRecordingStatus.OFF: {
  421. if (this.recordingType === 'jibri') {
  422. _requestLiveStreamId()
  423. .then(streamId => {
  424. this.eventEmitter.emit(
  425. UIEvents.RECORDING_TOGGLED,
  426. { streamId });
  427. sendAnalyticsEvent('recording.started');
  428. })
  429. .catch(reason => {
  430. if (reason === APP.UI.messageHandler.CANCEL) {
  431. sendAnalyticsEvent('recording.canceled');
  432. } else {
  433. logger.error(reason);
  434. }
  435. });
  436. } else {
  437. if (this.predefinedToken) {
  438. this.eventEmitter.emit(
  439. UIEvents.RECORDING_TOGGLED,
  440. { token: this.predefinedToken });
  441. sendAnalyticsEvent('recording.started');
  442. return;
  443. }
  444. _requestRecordingToken().then(token => {
  445. this.eventEmitter.emit(
  446. UIEvents.RECORDING_TOGGLED,
  447. { token });
  448. sendAnalyticsEvent('recording.started');
  449. })
  450. .catch(reason => {
  451. if (reason === APP.UI.messageHandler.CANCEL) {
  452. sendAnalyticsEvent('recording.canceled');
  453. } else {
  454. logger.error(reason);
  455. }
  456. });
  457. }
  458. break;
  459. }
  460. case JitsiRecordingStatus.BUSY: {
  461. APP.UI.messageHandler.showWarning({
  462. descriptionKey: this.recordingBusy,
  463. titleKey: this.recordingBusyTitle
  464. });
  465. break;
  466. }
  467. default: {
  468. APP.UI.messageHandler.showError({
  469. descriptionKey: this.recordingUnavailable,
  470. titleKey: this.recordingUnavailableTitle
  471. });
  472. }
  473. }
  474. },
  475. /**
  476. * Sets the toggled state of the recording toolbar button.
  477. *
  478. * @param {boolean} isToggled indicates if the button should be toggled
  479. * or not
  480. */
  481. _setToolbarButtonToggled(isToggled) {
  482. $('#toolbar_button_record').toggleClass('toggled', isToggled);
  483. }
  484. };
  485. export default Recording;