From af37141e3d819f06b68e33ca2342e25f8602be8e Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Mon, 24 Sep 2018 19:08:55 -0500 Subject: [PATCH] feat(dropbox): For mobile. This involves redesign of the web recording dialog in order to look the same as the mobile one. --- css/_recording.scss | 19 +- lang/main.json | 3 +- react/features/dropbox/actions.js | 48 +--- react/features/dropbox/functions.any.js | 50 ++++ react/features/dropbox/functions.js | 39 ---- react/features/dropbox/functions.native.js | 52 +++++ react/features/dropbox/functions.web.js | 112 +++++++++ react/features/dropbox/index.js | 2 +- .../Recording/AbstractRecordButton.js | 4 +- .../Recording/StartRecordingDialog.js | 4 +- .../Recording/StartRecordingDialogContent.js | 216 ++++++++++++++++++ .../StartRecordingDialogContent.native.js | 38 --- .../StartRecordingDialogContent.web.js | 194 ---------------- .../components/Recording/styles.native.js | 43 ++++ .../components/Recording/styles.web.js | 3 + react/features/recording/functions.js | 12 + 16 files changed, 518 insertions(+), 321 deletions(-) create mode 100644 react/features/dropbox/functions.any.js delete mode 100644 react/features/dropbox/functions.js create mode 100644 react/features/dropbox/functions.native.js create mode 100644 react/features/dropbox/functions.web.js create mode 100644 react/features/recording/components/Recording/StartRecordingDialogContent.js delete mode 100644 react/features/recording/components/Recording/StartRecordingDialogContent.native.js delete mode 100644 react/features/recording/components/Recording/StartRecordingDialogContent.web.js create mode 100644 react/features/recording/components/Recording/styles.native.js create mode 100644 react/features/recording/components/Recording/styles.web.js diff --git a/css/_recording.scss b/css/_recording.scss index 58c8b12848..3c74912672 100644 --- a/css/_recording.scss +++ b/css/_recording.scss @@ -3,8 +3,23 @@ } .recording-dialog { + flex: 0; + flex-direction: column; + + .recording-header { + display: flex; + flex: 0; + flex-direction: row; + justify-content: space-between; + align-items: center; + + .recording-title { + font-size: 16px; + font-weight: bold; + } + } + .authorization-panel { - border-bottom: 2px solid rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; margin-bottom: 10px; @@ -32,7 +47,7 @@ } } - .logged-in-pannel { + .logged-in-panel { padding: 10px; } } diff --git a/lang/main.json b/lang/main.json index b8f3a9704c..a7aa6e89fa 100644 --- a/lang/main.json +++ b/lang/main.json @@ -468,8 +468,7 @@ "on": "Recording", "pending": "Preparing to record the meeting...", "rec": "REC", - "authDropboxText": "Upload your recording to Dropbox.", - "authDropboxCompletedText": "Your recording file will appear in your Dropbox shortly after the recording has finished.", + "authDropboxText": "Upload to Dropbox", "serviceName": "Recording service", "signOut": "Sign Out", "signIn": "sign in", diff --git a/react/features/dropbox/actions.js b/react/features/dropbox/actions.js index ab66e5d289..edca7c8005 100644 --- a/react/features/dropbox/actions.js +++ b/react/features/dropbox/actions.js @@ -1,40 +1,9 @@ // @flow -import { Dropbox } from 'dropbox'; - -import { - getJitsiMeetGlobalNS, - getLocationContextRoot, - parseStandardURIString -} from '../base/util'; -import { parseURLParams } from '../base/config'; +import { getLocationContextRoot } from '../base/util'; import { UPDATE_DROPBOX_TOKEN } from './actionTypes'; - -/** - * Executes the oauth flow. - * - * @param {string} authUrl - The URL to oauth service. - * @returns {Promise} - The URL with the authorization details. - */ -function authorize(authUrl: string): Promise { - const windowName = `oauth${Date.now()}`; - const gloabalNS = getJitsiMeetGlobalNS(); - - gloabalNS.oauthCallbacks = gloabalNS.oauthCallbacks || {}; - - return new Promise(resolve => { - const popup = window.open(authUrl, windowName); - - gloabalNS.oauthCallbacks[windowName] = () => { - const returnURL = popup.location.href; - - popup.close(); - delete gloabalNS.oauthCallbacks.windowName; - resolve(returnURL); - }; - }); -} +import { _authorizeDropbox } from './functions'; /** * Action to authorize the Jitsi Recording app in dropbox. @@ -45,18 +14,13 @@ export function authorizeDropbox() { return (dispatch: Function, getState: Function) => { const state = getState(); const { locationURL } = state['features/base/connection']; - const { dropbox } = state['features/base/config']; + const { dropbox = {} } = state['features/base/config']; const redirectURI = `${locationURL.origin + getLocationContextRoot(locationURL)}static/oauth.html`; - const dropboxAPI = new Dropbox({ clientId: dropbox.clientId }); - const url = dropboxAPI.getAuthenticationUrl(redirectURI); - - authorize(url).then(returnUrl => { - const params - = parseURLParams(parseStandardURIString(returnUrl), true) || {}; - dispatch(updateDropboxToken(params.access_token)); - }); + _authorizeDropbox(dropbox.clientId, redirectURI) + .then( + token => dispatch(updateDropboxToken(token))); }; } diff --git a/react/features/dropbox/functions.any.js b/react/features/dropbox/functions.any.js new file mode 100644 index 0000000000..8d0f824ed5 --- /dev/null +++ b/react/features/dropbox/functions.any.js @@ -0,0 +1,50 @@ +// @flow +export * from './functions'; + +import { getDisplayName, getSpaceUsage } from './functions'; + +const logger = require('jitsi-meet-logger').getLogger(__filename); + +/** + * Information related to the user's dropbox account. + */ +type DropboxUserData = { + + /** + * The available space left in MB into the user's Dropbox account. + */ + spaceLeft: number, + + /** + * The display name of the user in Dropbox. + */ + userName: string +}; + +/** + * Fetches information about the user's dropbox account. + * + * @param {string} token - The dropbox access token. + * @param {string} clientId - The Jitsi Recorder dropbox app ID. + * @returns {Promise} + */ +export function getDropboxData( + token: string, + clientId: string +): Promise { + return Promise.all( + [ getDisplayName(token, clientId), getSpaceUsage(token, clientId) ] + ).then(([ userName, space ]) => { + const { allocated, used } = space; + + return { + userName, + spaceLeft: Math.floor((allocated - used) / 1048576)// 1MiB=1048576B + }; + + }, error => { + logger.error(error); + + return undefined; + }); +} diff --git a/react/features/dropbox/functions.js b/react/features/dropbox/functions.js deleted file mode 100644 index 156c544c47..0000000000 --- a/react/features/dropbox/functions.js +++ /dev/null @@ -1,39 +0,0 @@ -// @flow - -import { Dropbox } from 'dropbox'; - -const logger = require('jitsi-meet-logger').getLogger(__filename); - -/** - * Fetches information about the user's dropbox account. - * - * @param {string} token - The dropbox access token. - * @param {string} clientId - The Jitsi Recorder dropbox app ID. - * @returns {Promise} - */ -export function getDropboxData( - token: string, - clientId: string -): Promise { - const dropboxAPI = new Dropbox({ - accessToken: token, - clientId - }); - - return Promise.all( - [ dropboxAPI.usersGetCurrentAccount(), dropboxAPI.usersGetSpaceUsage() ] - ).then(([ account, space ]) => { - const { allocation, used } = space; - const { allocated } = allocation; - - return { - userName: account.name.display_name, - spaceLeft: Math.floor((allocated - used) / 1048576)// 1MiB=1048576B - }; - - }, error => { - logger.error(error); - - return undefined; - }); -} diff --git a/react/features/dropbox/functions.native.js b/react/features/dropbox/functions.native.js new file mode 100644 index 0000000000..593d64ca89 --- /dev/null +++ b/react/features/dropbox/functions.native.js @@ -0,0 +1,52 @@ +// @flow + +import { NativeModules } from 'react-native'; + +const { Dropbox } = NativeModules; + +/** + * Returns the display name for the current dropbox account. + * + * @param {string} token - The dropbox access token. + * @returns {Promise} - The promise will be resolved with the display + * name or rejected with an error. + */ +export function getDisplayName(token: string) { + return Dropbox.getDisplayName(token); +} + +/** + * Returns information about the space usage for the current dropbox account. + * + * @param {string} token - The dropbox access token. + * @returns {Promise<{ used: number, allocated: number}>} - The promise will be + * resolved with the object with information about the space usage (the used + * space and the allocated space) for the current dropbox account or rejected + * with an error. + */ +export function getSpaceUsage(token: string) { + return Dropbox.getSpaceUsage(token); +} + + +/** + * Action to authorize the Jitsi Recording app in dropbox. + * + * @param {string} clientId - The Jitsi Recorder dropbox app ID. + * @param {string} redirectURI - The return URL. + * @returns {Promise} - The promise will be resolved with the dropbox + * access token or rejected with an error. + */ +export function _authorizeDropbox(): Promise { + return Dropbox.authorize(); +} + +/** + * Returns true if the dropbox features is enabled and false + * otherwise. + * + * @returns {boolean} + */ +export function isEnabled() { + return Dropbox.ENABLED; +} diff --git a/react/features/dropbox/functions.web.js b/react/features/dropbox/functions.web.js new file mode 100644 index 0000000000..07fbf87435 --- /dev/null +++ b/react/features/dropbox/functions.web.js @@ -0,0 +1,112 @@ +// @flow + +import { Dropbox } from 'dropbox'; + +import { + getJitsiMeetGlobalNS, + parseStandardURIString +} from '../base/util'; +import { parseURLParams } from '../base/config'; + +/** + * Returns the display name for the current dropbox account. + * + * @param {string} token - The dropbox access token. + * @param {string} clientId - The Jitsi Recorder dropbox app ID. + * @returns {Promise} + */ +export function getDisplayName(token: string, clientId: string) { + const dropboxAPI = new Dropbox({ + accessToken: token, + clientId + }); + + return ( + dropboxAPI.usersGetCurrentAccount() + .then(account => account.name.display_name)); +} + +/** + * Returns information about the space usage for the current dropbox account. + * + * @param {string} token - The dropbox access token. + * @param {string} clientId - The Jitsi Recorder dropbox app ID. + * @returns {Promise} + */ +export function getSpaceUsage(token: string, clientId: string) { + const dropboxAPI = new Dropbox({ + accessToken: token, + clientId + }); + + return dropboxAPI.usersGetSpaceUsage().then(space => { + const { allocation, used } = space; + const { allocated } = allocation; + + return { + used, + allocated + }; + }); +} + + +/** + * Executes the oauth flow. + * + * @param {string} authUrl - The URL to oauth service. + * @returns {Promise} - The URL with the authorization details. + */ +function authorize(authUrl: string): Promise { + const windowName = `oauth${Date.now()}`; + const gloabalNS = getJitsiMeetGlobalNS(); + + gloabalNS.oauthCallbacks = gloabalNS.oauthCallbacks || {}; + + return new Promise(resolve => { + const popup = window.open(authUrl, windowName); + + gloabalNS.oauthCallbacks[windowName] = () => { + const returnURL = popup.location.href; + + popup.close(); + delete gloabalNS.oauthCallbacks.windowName; + resolve(returnURL); + }; + }); +} + +/** + * Action to authorize the Jitsi Recording app in dropbox. + * + * @param {string} clientId - The Jitsi Recorder dropbox app ID. + * @param {string} redirectURI - The return URL. + * @returns {Promise} + */ +export function _authorizeDropbox( + clientId: string, + redirectURI: string +): Promise { + const dropboxAPI = new Dropbox({ clientId }); + const url = dropboxAPI.getAuthenticationUrl(redirectURI); + + return authorize(url).then(returnUrl => { + const params + = parseURLParams(parseStandardURIString(returnUrl), true) || {}; + + return params.access_token; + }); +} + +/** + * Returns true if the dropbox features is enabled and false + * otherwise. + * + * @param {Object} state - The redux state. + * @returns {boolean} + */ +export function isEnabled(state: Object) { + const { dropbox = {} } = state['features/base/config']; + + return typeof dropbox.clientId === 'string'; +} diff --git a/react/features/dropbox/index.js b/react/features/dropbox/index.js index 20b2cbc745..274d331371 100644 --- a/react/features/dropbox/index.js +++ b/react/features/dropbox/index.js @@ -1,4 +1,4 @@ export * from './actions'; -export * from './functions'; +export * from './functions.any'; import './reducer'; diff --git a/react/features/recording/components/Recording/AbstractRecordButton.js b/react/features/recording/components/Recording/AbstractRecordButton.js index ac0b68030a..ee5e99bf8c 100644 --- a/react/features/recording/components/Recording/AbstractRecordButton.js +++ b/react/features/recording/components/Recording/AbstractRecordButton.js @@ -11,6 +11,7 @@ import { getLocalParticipant, isLocalParticipantModerator } from '../../../base/participants'; +import { isEnabled as isDropboxEnabled } from '../../../dropbox'; import { AbstractButton, type AbstractButtonProps @@ -123,7 +124,6 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object { // its own to be visible or not. const isModerator = isLocalParticipantModerator(state); const { - dropbox = {}, enableFeaturesBasedOnToken, fileRecordingsEnabled } = state['features/base/config']; @@ -131,7 +131,7 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object { visible = isModerator && fileRecordingsEnabled - && typeof dropbox.clientId === 'string'; + && isDropboxEnabled(state); if (enableFeaturesBasedOnToken) { visible = visible && String(features.recording) === 'true'; diff --git a/react/features/recording/components/Recording/StartRecordingDialog.js b/react/features/recording/components/Recording/StartRecordingDialog.js index 917ba9d9cd..720497b4ee 100644 --- a/react/features/recording/components/Recording/StartRecordingDialog.js +++ b/react/features/recording/components/Recording/StartRecordingDialog.js @@ -222,8 +222,10 @@ class StartRecordingDialog extends Component { * }} */ function mapStateToProps(state: Object) { + const { dropbox = {} } = state['features/base/config']; + return { - _clientId: state['features/base/config'].dropbox.clientId, + _clientId: dropbox.clientId, _conference: state['features/base/conference'].conference, _token: state['features/dropbox'].token }; diff --git a/react/features/recording/components/Recording/StartRecordingDialogContent.js b/react/features/recording/components/Recording/StartRecordingDialogContent.js new file mode 100644 index 0000000000..b30e009aca --- /dev/null +++ b/react/features/recording/components/Recording/StartRecordingDialogContent.js @@ -0,0 +1,216 @@ +// @flow + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { + createRecordingDialogEvent, + sendAnalytics +} from '../../../analytics'; +import { translate } from '../../../base/i18n'; +import { + Container, + LoadingIndicator, + Switch, + Text +} from '../../../base/react'; +import { authorizeDropbox, updateDropboxToken } from '../../../dropbox'; + +import styles from './styles'; +import { getRecordingDurationEstimation } from '../../functions'; + +type Props = { + + /** + * The redux dispatch function. + */ + dispatch: Function, + + /** + * true if we have valid oauth token. + */ + isTokenValid: boolean, + + /** + * true if we are in process of validating the oauth token. + */ + isValidating: boolean, + + /** + * Number of MiB of available space in user's Dropbox account. + */ + spaceLeft: ?number, + + /** + * The translate function. + */ + t: Function, + + /** + * The display name of the user's Dropbox account. + */ + userName: ?string, +}; + +/** + * React Component for getting confirmation to start a file recording session. + * + * @extends Component + */ +class StartRecordingDialogContent extends Component { + /** + * Initializes a new {@code StartRecordingDialogContent} instance. + * + * @inheritdoc + */ + constructor(props) { + super(props); + + // Bind event handler so it is only bound once for every instance. + this._signIn = this._signIn.bind(this); + this._signOut = this._signOut.bind(this); + this._onSwitchChange = this._onSwitchChange.bind(this); + } + + /** + * Renders the component. + * + * @protected + * @returns {React$Component} + */ + render() { + const { isTokenValid, isValidating, t } = this.props; + + let content = null; + + if (isValidating) { + content = this._renderSpinner(); + } else if (isTokenValid) { + content = this._renderSignOut(); + } + + // else { // Sign in screen: + // We don't need to render any additional information. + // } + + return ( + + + + { t('recording.authDropboxText') } + + + + + { content } + + + ); + } + + _onSwitchChange: boolean => void; + + /** + * Handler for onValueChange events from the Switch component. + * + * @returns {void} + */ + _onSwitchChange() { + if (this.props.isTokenValid) { + this._signOut(); + } else { + this._signIn(); + } + } + + /** + * Renders a spinner component. + * + * @returns {React$Component} + */ + _renderSpinner() { + return ( + + ); + } + + /** + * Renders the screen with the account information of a logged in user. + * + * @returns {React$Component} + */ + _renderSignOut() { + const { spaceLeft, t, userName } = this.props; + const duration = getRecordingDurationEstimation(spaceLeft); + + return ( + + + + + { t('recording.loggedIn', { userName }) } + + + + + { + t('recording.availableSpace', { + spaceLeft, + duration + }) + } + + + + + { t('recording.startRecordingBody') } + + + ); + } + + _signIn: () => {}; + + /** + * Sings in a user. + * + * @returns {void} + */ + _signIn() { + sendAnalytics( + createRecordingDialogEvent('start', 'signIn.button') + ); + this.props.dispatch(authorizeDropbox()); + } + + _signOut: () => {}; + + /** + * Sings out an user from dropbox. + * + * @returns {void} + */ + _signOut() { + sendAnalytics( + createRecordingDialogEvent('start', 'signOut.button') + ); + this.props.dispatch(updateDropboxToken()); + } +} + +export default translate(connect()(StartRecordingDialogContent)); diff --git a/react/features/recording/components/Recording/StartRecordingDialogContent.native.js b/react/features/recording/components/Recording/StartRecordingDialogContent.native.js deleted file mode 100644 index e67488e368..0000000000 --- a/react/features/recording/components/Recording/StartRecordingDialogContent.native.js +++ /dev/null @@ -1,38 +0,0 @@ -// @flow - -import React, { Component } from 'react'; - -import { DialogContent } from '../../../base/dialog'; -import { translate } from '../../../base/i18n'; - -type Props = { - - /** - * Invoked to obtain translated strings. - */ - t: Function -}; - -/** - * React Component for getting confirmation to start a file recording session. - * - * @extends Component - */ -class StartRecordingDialogContent extends Component { - /** - * Renders the platform specific dialog content. - * - * @returns {void} - */ - render() { - const { t } = this.props; - - return ( - - { t('recording.startRecordingBody') } - - ); - } -} - -export default translate(StartRecordingDialogContent); diff --git a/react/features/recording/components/Recording/StartRecordingDialogContent.web.js b/react/features/recording/components/Recording/StartRecordingDialogContent.web.js deleted file mode 100644 index da3ea8033d..0000000000 --- a/react/features/recording/components/Recording/StartRecordingDialogContent.web.js +++ /dev/null @@ -1,194 +0,0 @@ -// @flow - -import Spinner from '@atlaskit/spinner'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; - -import { - createRecordingDialogEvent, - sendAnalytics -} from '../../../analytics'; -import { translate } from '../../../base/i18n'; -import { authorizeDropbox, updateDropboxToken } from '../../../dropbox'; - -type Props = { - - /** - * The redux dispatch function. - */ - dispatch: Function, - - /** - * true if we have valid oauth token. - */ - isTokenValid: boolean, - - /** - * true if we are in process of validating the oauth token. - */ - isValidating: boolean, - - /** - * Number of MiB of available space in user's Dropbox account. - */ - spaceLeft: ?number, - - /** - * The translate function. - */ - t: Function, - - /** - * The display name of the user's Dropbox account. - */ - userName: ?string, -}; - -/** - * React Component for getting confirmation to start a file recording session. - * - * @extends Component - */ -class StartRecordingDialogContent extends Component { - /** - * Initializes a new {@code StartRecordingDialogContent} instance. - * - * @inheritdoc - */ - constructor(props) { - super(props); - - // Bind event handler so it is only bound once for every instance. - this._onSignInClick = this._onSignInClick.bind(this); - this._onSignOutClick = this._onSignOutClick.bind(this); - } - - /** - * Renders the platform specific dialog content. - * - * @protected - * @returns {React$Component} - */ - render() { - const { isTokenValid, isValidating, t } = this.props; - - let content = null; - - if (isValidating) { - content = this._renderSpinner(); - } else if (isTokenValid) { - content = this._renderSignOut(); - } else { - content = this._renderSignIn(); - } - - return ( -
-
- { content } -
-
{ t('recording.startRecordingBody') }
-
- ); - } - - /** - * Renders a spinner component. - * - * @returns {React$Component} - */ - _renderSpinner() { - return ( - - ); - } - - /** - * Renders the sign in screen. - * - * @returns {React$Component} - */ - _renderSignIn() { - const { t } = this.props; - - return ( -
-
{ t('recording.authDropboxText') }
-
- - { t('recording.signIn') } -
-
- ); - } - - /** - * Renders the screen with the account information of a logged in user. - * - * @returns {React$Component} - */ - _renderSignOut() { - const { spaceLeft, t, userName } = this.props; - - return ( -
-
{ t('recording.authDropboxCompletedText') }
-
-
- { t('recording.loggedIn', { userName }) } (  - - { t('recording.signOut') } - -  ) -
-
- { - t('recording.availableSpace', { - spaceLeft, - - // assuming 1min -> 10MB recording: - duration: Math.floor((spaceLeft || 0) / 10) - }) - } -
-
-
- ); - } - - _onSignInClick: () => {}; - - /** - * Handles click events for the dropbox sign in button. - * - * @returns {void} - */ - _onSignInClick() { - sendAnalytics( - createRecordingDialogEvent('start', 'signIn.button') - ); - this.props.dispatch(authorizeDropbox()); - } - - _onSignOutClick: () => {}; - - /** - * Sings out an user from dropbox. - * - * @returns {void} - */ - _onSignOutClick() { - sendAnalytics( - createRecordingDialogEvent('start', 'signOut.button') - ); - this.props.dispatch(updateDropboxToken()); - } -} - -export default translate(connect()(StartRecordingDialogContent)); diff --git a/react/features/recording/components/Recording/styles.native.js b/react/features/recording/components/Recording/styles.native.js new file mode 100644 index 0000000000..5af40fafd8 --- /dev/null +++ b/react/features/recording/components/Recording/styles.native.js @@ -0,0 +1,43 @@ +// @flow + +import { BoxModel, createStyleSheet } from '../../../base/styles'; + +// XXX The "standard" {@code BoxModel.padding} has been deemed insufficient in +// the special case(s) of the recording feature bellow. +const _PADDING = BoxModel.padding * 1.5; + +/** + * The styles of the React {@code Components} of the feature recording. + */ +export default createStyleSheet({ + container: { + flex: 0, + flexDirection: 'column' + }, + + header: { + alignItems: 'center', + flex: 0, + flexDirection: 'row', + justifyContent: 'space-between', + paddingBottom: _PADDING, + paddingTop: _PADDING + }, + + loggedIn: { + paddingBottom: _PADDING + }, + + startRecordingText: { + paddingBottom: _PADDING + }, + + switch: { + paddingRight: BoxModel.padding + }, + + title: { + fontSize: 16, + fontWeight: 'bold' + } +}); diff --git a/react/features/recording/components/Recording/styles.web.js b/react/features/recording/components/Recording/styles.web.js new file mode 100644 index 0000000000..4100381751 --- /dev/null +++ b/react/features/recording/components/Recording/styles.web.js @@ -0,0 +1,3 @@ +// XXX CSS is used on Web, JavaScript styles are use only for mobile. Export an +// (empty) object so that styles[*] statements on Web don't trigger errors. +export default {}; diff --git a/react/features/recording/functions.js b/react/features/recording/functions.js index 7ef67c7de6..8f543a0634 100644 --- a/react/features/recording/functions.js +++ b/react/features/recording/functions.js @@ -19,6 +19,18 @@ export function getActiveSession(state: Object, mode: string) { || sessionData.status === statusConstants.PENDING)); } +/** + * Returns an estimated recording duration based on the size of the video file + * in MB. The estimate is calculated under the assumption that 1 min of recorded + * video needs 10MB of storage on avarage. + * + * @param {number} size - The size in MB of the recorded video. + * @returns {number} - The estimated duration in minutes. + */ +export function getRecordingDurationEstimation(size: ?number) { + return Math.floor((size || 0) / 10); +} + /** * Searches in the passed in redux state for a recording session that matches * the passed in recording session ID.