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

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