Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

Recording.js 18KB

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