Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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