選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

Recording.js 18KB

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