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

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