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

Recording.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  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. const logger = require("jitsi-meet-logger").getLogger(__filename);
  18. import UIEvents from "../../../service/UI/UIEvents";
  19. import UIUtil from '../util/UIUtil';
  20. import VideoLayout from '../videolayout/VideoLayout';
  21. import { setToolboxEnabled } from '../../../react/features/toolbox';
  22. import { setNotificationsEnabled } from '../../../react/features/notifications';
  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 (
  35. interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1
  36. && config.enableRecording
  37. && APP.conference.isRecordingSupported());
  38. }
  39. /**
  40. * Request live stream token from the user.
  41. * @returns {Promise}
  42. */
  43. function _requestLiveStreamId() {
  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. const streamIdHelp
  53. = APP.translation.generateTranslationHTML(
  54. "liveStreaming.streamIdHelp");
  55. return new Promise(function (resolve, reject) {
  56. dialog = APP.UI.messageHandler.openDialogWithStates({
  57. state0: {
  58. titleKey: "dialog.liveStreaming",
  59. html:
  60. `<input class="input-control"
  61. name="streamId" type="text"
  62. data-i18n="[placeholder]dialog.streamKey"
  63. autofocus><div style="text-align: right">
  64. <a class="helper-link" target="_new"
  65. href="${interfaceConfig.LIVE_STREAMING_HELP_LINK}">`
  66. + streamIdHelp
  67. + `</a></div>`,
  68. persistent: false,
  69. buttons: [
  70. {title: cancelButton, value: false},
  71. {title: startStreamingButton, value: true}
  72. ],
  73. focus: ':input:first',
  74. defaultButton: 1,
  75. submit: function (e, v, m, f) {
  76. e.preventDefault();
  77. if (v) {
  78. if (f.streamId && f.streamId.length > 0) {
  79. resolve(UIUtil.escapeHtml(f.streamId));
  80. dialog.close();
  81. return;
  82. }
  83. else {
  84. dialog.goToState('state1');
  85. return false;
  86. }
  87. } else {
  88. reject(APP.UI.messageHandler.CANCEL);
  89. dialog.close();
  90. return false;
  91. }
  92. }
  93. },
  94. state1: {
  95. titleKey: "dialog.liveStreaming",
  96. html: streamIdRequired,
  97. persistent: false,
  98. buttons: [
  99. {title: cancelButton, value: false},
  100. {title: backButton, value: true}
  101. ],
  102. focus: ':input:first',
  103. defaultButton: 1,
  104. submit: function (e, v) {
  105. e.preventDefault();
  106. if (v === 0) {
  107. reject(APP.UI.messageHandler.CANCEL);
  108. dialog.close();
  109. } else {
  110. dialog.goToState('state0');
  111. }
  112. }
  113. }
  114. }, {
  115. close: function () {
  116. dialog = null;
  117. }
  118. });
  119. });
  120. }
  121. /**
  122. * Request recording token from the user.
  123. * @returns {Promise}
  124. */
  125. function _requestRecordingToken() {
  126. let titleKey = "dialog.recordingToken";
  127. let msgString = (
  128. `<input name="recordingToken" type="text"
  129. data-i18n="[placeholder]dialog.token"
  130. class="input-control"
  131. autofocus>`
  132. );
  133. return new Promise(function (resolve, reject) {
  134. dialog = APP.UI.messageHandler.openTwoButtonDialog({
  135. titleKey,
  136. msgString,
  137. leftButtonKey: 'dialog.Save',
  138. submitFunction: function (e, v, m, f) {
  139. if (v && f.recordingToken) {
  140. resolve(UIUtil.escapeHtml(f.recordingToken));
  141. } else {
  142. reject(APP.UI.messageHandler.CANCEL);
  143. }
  144. },
  145. closeFunction: function () {
  146. dialog = null;
  147. },
  148. focus: ':input:first'
  149. });
  150. });
  151. }
  152. /**
  153. * Shows a prompt dialog to the user when they have toggled off the recording.
  154. *
  155. * @param recordingType the recording type
  156. * @returns {Promise}
  157. * @private
  158. */
  159. function _showStopRecordingPrompt(recordingType) {
  160. var title;
  161. var message;
  162. var buttonKey;
  163. if (recordingType === "jibri") {
  164. title = "dialog.liveStreaming";
  165. message = "dialog.stopStreamingWarning";
  166. buttonKey = "dialog.stopLiveStreaming";
  167. }
  168. else {
  169. title = "dialog.recording";
  170. message = "dialog.stopRecordingWarning";
  171. buttonKey = "dialog.stopRecording";
  172. }
  173. return new Promise((resolve, reject) => {
  174. dialog = APP.UI.messageHandler.openTwoButtonDialog({
  175. titleKey: title,
  176. msgKey: message,
  177. leftButtonKey: buttonKey,
  178. submitFunction: (e, v) => (v ? resolve : reject)(),
  179. closeFunction: () => {
  180. dialog = null;
  181. }
  182. });
  183. });
  184. }
  185. /**
  186. * Moves the element given by {selector} to the top right corner of the screen.
  187. * Set additional classes that can be used to style the selector relative to the
  188. * state of the filmstrip.
  189. *
  190. * @param selector the selector for the element to move
  191. * @param move {true} to move the element, {false} to move it back to its intial
  192. * position
  193. */
  194. function moveToCorner(selector, move) {
  195. let moveToCornerClass = "moveToCorner";
  196. let containsClass = selector.hasClass(moveToCornerClass);
  197. if (move && !containsClass)
  198. selector.addClass(moveToCornerClass);
  199. else if (!move && containsClass)
  200. selector.removeClass(moveToCornerClass);
  201. const {
  202. remoteVideosVisible,
  203. visible
  204. } = APP.store.getState()['features/filmstrip'];
  205. const filmstripWasHidden = selector.hasClass('without-filmstrip');
  206. const filmstipIsOpening = filmstripWasHidden && visible;
  207. selector.toggleClass('opening', filmstipIsOpening);
  208. selector.toggleClass('with-filmstrip', visible);
  209. selector.toggleClass('without-filmstrip', !visible);
  210. selector.toggleClass('with-remote-videos', remoteVideosVisible);
  211. selector.toggleClass('without-remote-videos', !remoteVideosVisible);
  212. }
  213. /**
  214. * The status of the recorder.
  215. * FIXME: Those constants should come from the library.
  216. * @type {{ON: string, OFF: string, AVAILABLE: string,
  217. * UNAVAILABLE: string, PENDING: string}}
  218. */
  219. var Status = {
  220. ON: "on",
  221. OFF: "off",
  222. AVAILABLE: "available",
  223. UNAVAILABLE: "unavailable",
  224. PENDING: "pending",
  225. RETRYING: "retrying",
  226. ERROR: "error",
  227. FAILED: "failed",
  228. BUSY: "busy"
  229. };
  230. /**
  231. * Checks whether if the given status is either PENDING or RETRYING
  232. * @param status {Status} Jibri status to be checked
  233. * @returns {boolean} true if the condition is met or false otherwise.
  234. */
  235. function isStartingStatus(status) {
  236. return status === Status.PENDING || status === Status.RETRYING;
  237. }
  238. /**
  239. * Manages the recording user interface and user experience.
  240. * @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
  241. * updateRecordingUI, checkAutoRecord}}
  242. */
  243. var Recording = {
  244. /**
  245. * Initializes the recording UI.
  246. */
  247. init(eventEmitter, recordingType) {
  248. this.eventEmitter = eventEmitter;
  249. this.recordingType = recordingType;
  250. this.updateRecordingState(APP.conference.getRecordingState());
  251. if (recordingType === 'jibri') {
  252. this.baseClass = "fa fa-play-circle";
  253. this.recordingTitle = "dialog.liveStreaming";
  254. this.recordingOnKey = "liveStreaming.on";
  255. this.recordingOffKey = "liveStreaming.off";
  256. this.recordingPendingKey = "liveStreaming.pending";
  257. this.failedToStartKey = "liveStreaming.failedToStart";
  258. this.recordingErrorKey = "liveStreaming.error";
  259. this.recordingButtonTooltip = "liveStreaming.buttonTooltip";
  260. this.recordingUnavailable = "liveStreaming.unavailable";
  261. this.recordingBusy = "liveStreaming.busy";
  262. }
  263. else {
  264. this.baseClass = "icon-recEnable";
  265. this.recordingTitle = "dialog.recording";
  266. this.recordingOnKey = "recording.on";
  267. this.recordingOffKey = "recording.off";
  268. this.recordingPendingKey = "recording.pending";
  269. this.failedToStartKey = "recording.failedToStart";
  270. this.recordingErrorKey = "recording.error";
  271. this.recordingButtonTooltip = "recording.buttonTooltip";
  272. this.recordingUnavailable = "recording.unavailable";
  273. this.recordingBusy = "liveStreaming.busy";
  274. }
  275. // XXX Due to the React-ification of Toolbox, the HTMLElement with id
  276. // toolbar_button_record may not exist yet.
  277. $(document).on(
  278. 'click',
  279. '#toolbar_button_record',
  280. ev => this._onToolbarButtonClick(ev));
  281. // If I am a recorder then I publish my recorder custom role to notify
  282. // everyone.
  283. if (config.iAmRecorder) {
  284. VideoLayout.enableDeviceAvailabilityIcons(
  285. APP.conference.getMyUserId(), false);
  286. VideoLayout.setLocalVideoVisible(false);
  287. APP.store.dispatch(setToolboxEnabled(false));
  288. APP.store.dispatch(setNotificationsEnabled(false));
  289. APP.UI.messageHandler.enablePopups(false);
  290. }
  291. this.eventEmitter.addListener(UIEvents.UPDATED_FILMSTRIP_DISPLAY, () =>{
  292. this._updateStatusLabel();
  293. });
  294. },
  295. /**
  296. * Initialise the recording button.
  297. */
  298. initRecordingButton() {
  299. const selector = $('#toolbar_button_record');
  300. UIUtil.setTooltip(selector, 'liveStreaming.buttonTooltip', 'right');
  301. selector.addClass(this.baseClass);
  302. selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip);
  303. APP.translation.translateElement(selector);
  304. },
  305. /**
  306. * Shows or hides the 'recording' button.
  307. * @param show {true} to show the recording button, {false} to hide it
  308. */
  309. showRecordingButton(show) {
  310. let shouldShow = show && _isRecordingButtonEnabled();
  311. let id = 'toolbar_button_record';
  312. UIUtil.setVisible(id, shouldShow);
  313. },
  314. /**
  315. * Updates the recording state UI.
  316. * @param recordingState gives us the current recording state
  317. */
  318. updateRecordingState(recordingState) {
  319. // I'm the recorder, so I don't want to see any UI related to states.
  320. if (config.iAmRecorder)
  321. return;
  322. // If there's no state change, we ignore the update.
  323. if (!recordingState || this.currentState === recordingState)
  324. return;
  325. this.updateRecordingUI(recordingState);
  326. },
  327. /**
  328. * Sets the state of the recording button.
  329. * @param recordingState gives us the current recording state
  330. */
  331. updateRecordingUI(recordingState) {
  332. let oldState = this.currentState;
  333. this.currentState = recordingState;
  334. // TODO: handle recording state=available
  335. if (recordingState === Status.ON ||
  336. recordingState === Status.RETRYING) {
  337. this._setToolbarButtonToggled(true);
  338. this._updateStatusLabel(this.recordingOnKey, false);
  339. }
  340. else if (recordingState === Status.OFF
  341. || recordingState === Status.UNAVAILABLE
  342. || recordingState === Status.BUSY
  343. || recordingState === Status.FAILED) {
  344. // We don't want to do any changes if this is
  345. // an availability change.
  346. if (oldState !== Status.ON
  347. && !isStartingStatus(oldState))
  348. return;
  349. this._setToolbarButtonToggled(false);
  350. let messageKey;
  351. if (isStartingStatus(oldState))
  352. messageKey = this.failedToStartKey;
  353. else
  354. messageKey = this.recordingOffKey;
  355. this._updateStatusLabel(messageKey, true);
  356. setTimeout(function(){
  357. $('#recordingLabel').css({display: "none"});
  358. }, 5000);
  359. }
  360. else if (recordingState === Status.PENDING) {
  361. this._setToolbarButtonToggled(false);
  362. this._updateStatusLabel(this.recordingPendingKey, true);
  363. }
  364. else if (recordingState === Status.ERROR
  365. || recordingState === Status.FAILED) {
  366. this._setToolbarButtonToggled(false);
  367. this._updateStatusLabel(this.recordingErrorKey, true);
  368. }
  369. let labelSelector = $('#recordingLabel');
  370. // We don't show the label for available state.
  371. if (recordingState !== Status.AVAILABLE
  372. && !labelSelector.is(":visible"))
  373. labelSelector.css({display: "inline-block"});
  374. // Recording spinner
  375. let spinnerId = 'recordingSpinner';
  376. UIUtil.setVisible(
  377. spinnerId, recordingState === Status.RETRYING);
  378. },
  379. // checks whether recording is enabled and whether we have params
  380. // to start automatically recording
  381. checkAutoRecord() {
  382. if (_isRecordingButtonEnabled && config.autoRecord) {
  383. this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
  384. this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED,
  385. this.predefinedToken);
  386. }
  387. },
  388. /**
  389. * Updates the status label.
  390. * @param textKey the text to show
  391. * @param isCentered indicates if the label should be centered on the window
  392. * or moved to the top right corner.
  393. */
  394. _updateStatusLabel(textKey, isCentered) {
  395. let labelSelector = $('#recordingLabel');
  396. let labelTextSelector = $('#recordingLabelText');
  397. moveToCorner(labelSelector, !isCentered);
  398. labelTextSelector.attr("data-i18n", textKey);
  399. APP.translation.translateElement(labelSelector);
  400. },
  401. /**
  402. * Handles {@code click} on {@code toolbar_button_record}.
  403. *
  404. * @returns {void}
  405. */
  406. _onToolbarButtonClick() {
  407. if (dialog) {
  408. return;
  409. }
  410. JitsiMeetJS.analytics.sendEvent('recording.clicked');
  411. switch (this.currentState) {
  412. case Status.ON:
  413. case Status.RETRYING:
  414. case Status.PENDING: {
  415. _showStopRecordingPrompt(this.recordingType).then(
  416. () => {
  417. this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
  418. JitsiMeetJS.analytics.sendEvent('recording.stopped');
  419. },
  420. () => {});
  421. break;
  422. }
  423. case Status.AVAILABLE:
  424. case Status.OFF: {
  425. if (this.recordingType === 'jibri')
  426. _requestLiveStreamId().then(streamId => {
  427. this.eventEmitter.emit(
  428. UIEvents.RECORDING_TOGGLED,
  429. { streamId });
  430. JitsiMeetJS.analytics.sendEvent('recording.started');
  431. }).catch(reason => {
  432. if (reason !== APP.UI.messageHandler.CANCEL)
  433. logger.error(reason);
  434. else
  435. JitsiMeetJS.analytics.sendEvent('recording.canceled');
  436. });
  437. else {
  438. if (this.predefinedToken) {
  439. this.eventEmitter.emit(
  440. UIEvents.RECORDING_TOGGLED,
  441. { token: this.predefinedToken });
  442. JitsiMeetJS.analytics.sendEvent('recording.started');
  443. return;
  444. }
  445. _requestRecordingToken().then((token) => {
  446. this.eventEmitter.emit(
  447. UIEvents.RECORDING_TOGGLED,
  448. { token });
  449. JitsiMeetJS.analytics.sendEvent('recording.started');
  450. }).catch(reason => {
  451. if (reason !== APP.UI.messageHandler.CANCEL)
  452. logger.error(reason);
  453. else
  454. JitsiMeetJS.analytics.sendEvent('recording.canceled');
  455. });
  456. }
  457. break;
  458. }
  459. case Status.BUSY: {
  460. dialog = APP.UI.messageHandler.openMessageDialog(
  461. this.recordingTitle,
  462. this.recordingBusy,
  463. null,
  464. () => {
  465. dialog = null;
  466. }
  467. );
  468. break;
  469. }
  470. default: {
  471. dialog = APP.UI.messageHandler.openMessageDialog(
  472. this.recordingTitle,
  473. this.recordingUnavailable,
  474. null,
  475. () => {
  476. dialog = null;
  477. }
  478. );
  479. }
  480. }
  481. },
  482. /**
  483. * Sets the toggled state of the recording toolbar button.
  484. *
  485. * @param {boolean} isToggled indicates if the button should be toggled
  486. * or not
  487. */
  488. _setToolbarButtonToggled(isToggled) {
  489. $("#toolbar_button_record").toggleClass("toggled", isToggled);
  490. }
  491. };
  492. export default Recording;