Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

Recording.js 18KB

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