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.

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