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

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