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

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