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

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