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

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