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

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