您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

Recording.js 16KB

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