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 18KB

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