Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

Recording.js 18KB

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