From 07bc70c2f59ebc11781c6e9b98e08fe38edce53b Mon Sep 17 00:00:00 2001 From: Radium Zheng Date: Wed, 20 Jun 2018 02:43:33 +1000 Subject: [PATCH 01/41] Implement local recording index.js of local recording local-recording(ui): recording button local-recording(encoding): flac support with libflac.js Fixes in RecordingController; integration with UI local-recording(controller): coordinate recording on different clients local-recording(controller): allow recording on remote participants local-recording(controller): global singleton local-recording(controller): use middleware to init LocalRecording cleanup and documentation in RecordingController local-recording(refactor): "Delegate" -> "Adapter" code style stop eslint and flow from complaining temp save: client status fix linter issues fix some docs; remove global LocalRecording instance use node.js packaging for libflac.js; remove vendor/ folder code style: flacEncodeWorker.js use moment.js to do time diff remove the use of console.log code style: flac related files remove excessive empty lines; and more docs remove the use of clockTick for UI updates initalize flacEncodeWorker properly, to avoid premature audio data transmission move the realization of recordingController events from LocalRecordingButton to middleware i18n strings minor markup changes in LocalRecordingInfoDialog fix documentation --- Makefile | 13 +- lang/main.json | 23 + package-lock.json | 4 + package.json | 1 + react/features/local-recording/actionTypes.js | 40 ++ react/features/local-recording/actions.js | 59 +++ .../components/LocalRecordingButton.js | 111 ++++ .../components/LocalRecordingInfoDialog.js | 332 ++++++++++++ .../local-recording/components/index.js | 1 + .../controller/RecordingController.js | 493 ++++++++++++++++++ .../local-recording/controller/index.js | 1 + react/features/local-recording/index.js | 7 + react/features/local-recording/middleware.js | 52 ++ .../local-recording/recording/OggAdapter.js | 107 ++++ .../recording/RecordingAdapter.js | 41 ++ .../local-recording/recording/Utils.js | 34 ++ .../local-recording/recording/WavAdapter.js | 284 ++++++++++ .../recording/flac/FlacAdapter.js | 170 ++++++ .../recording/flac/flacEncodeWorker.js | 416 +++++++++++++++ .../local-recording/recording/flac/index.js | 1 + .../recording/flac/messageTypes.js | 44 ++ .../local-recording/recording/index.js | 4 + react/features/local-recording/reducer.js | 46 ++ .../toolbox/components/web/Toolbox.js | 29 ++ webpack.config.js | 6 +- 25 files changed, 2316 insertions(+), 3 deletions(-) create mode 100644 react/features/local-recording/actionTypes.js create mode 100644 react/features/local-recording/actions.js create mode 100644 react/features/local-recording/components/LocalRecordingButton.js create mode 100644 react/features/local-recording/components/LocalRecordingInfoDialog.js create mode 100644 react/features/local-recording/components/index.js create mode 100644 react/features/local-recording/controller/RecordingController.js create mode 100644 react/features/local-recording/controller/index.js create mode 100644 react/features/local-recording/index.js create mode 100644 react/features/local-recording/middleware.js create mode 100644 react/features/local-recording/recording/OggAdapter.js create mode 100644 react/features/local-recording/recording/RecordingAdapter.js create mode 100644 react/features/local-recording/recording/Utils.js create mode 100644 react/features/local-recording/recording/WavAdapter.js create mode 100644 react/features/local-recording/recording/flac/FlacAdapter.js create mode 100644 react/features/local-recording/recording/flac/flacEncodeWorker.js create mode 100644 react/features/local-recording/recording/flac/index.js create mode 100644 react/features/local-recording/recording/flac/messageTypes.js create mode 100644 react/features/local-recording/recording/index.js create mode 100644 react/features/local-recording/reducer.js diff --git a/Makefile b/Makefile index e86136866..6598ba2ae 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ BUILD_DIR = build CLEANCSS = ./node_modules/.bin/cleancss DEPLOY_DIR = libs LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/ +LIBFLAC_DIR = node_modules/libflac/dist/ NODE_SASS = ./node_modules/.bin/node-sass NPM = npm OUTPUT_DIR = . @@ -19,7 +20,7 @@ compile: clean: rm -fr $(BUILD_DIR) -deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-css deploy-local +deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local deploy-init: rm -fr $(DEPLOY_DIR) @@ -33,6 +34,8 @@ deploy-appbundle: $(BUILD_DIR)/do_external_connect.min.map \ $(BUILD_DIR)/external_api.min.js \ $(BUILD_DIR)/external_api.min.map \ + $(BUILD_DIR)/flacEncodeWorker.min.js \ + $(BUILD_DIR)/flacEncodeWorker.min.map \ $(BUILD_DIR)/device_selection_popup_bundle.min.js \ $(BUILD_DIR)/device_selection_popup_bundle.min.map \ $(BUILD_DIR)/dial_in_info_bundle.min.js \ @@ -50,6 +53,12 @@ deploy-lib-jitsi-meet: $(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \ $(DEPLOY_DIR) +deploy-libflac: + cp \ + $(LIBFLAC_DIR)/libflac3-1.3.2.min.js \ + $(LIBFLAC_DIR)/libflac3-1.3.2.min.js.mem \ + $(DEPLOY_DIR) + deploy-css: $(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \ $(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \ @@ -58,7 +67,7 @@ deploy-css: deploy-local: ([ ! -x deploy-local.sh ] || ./deploy-local.sh) -dev: deploy-init deploy-css deploy-lib-jitsi-meet +dev: deploy-init deploy-css deploy-lib-jitsi-meet deploy-libflac $(WEBPACK_DEV_SERVER) source-package: diff --git a/lang/main.json b/lang/main.json index 2d5f356f6..8c5bc4b6f 100644 --- a/lang/main.json +++ b/lang/main.json @@ -666,5 +666,28 @@ "decline": "Dismiss", "productLabel": "from Jitsi Meet", "videoCallTitle": "Incoming video call" + }, + "localRecording": { + "localRecording": "Local Recording", + "dialogTitle": "Local Recording Controls", + "start": "Start", + "stop": "Stop", + "moderator": "Moderator", + "localUser": "Local user", + "duration": "Duration", + "encoding": "Encoding", + "participantStats": "Participant Stats", + "clientState": { + "on": "On", + "off": "Off", + "unknown": "Unknown" + }, + "messages": { + "engaged": "Local recording engaged.", + "finished": "Recording session __token__ finished. Please send the recorded file to the moderator.", + "notModerator": "You are not the moderator. You cannot start or stop local recording." + }, + "yes": "Yes", + "no": "No" } } diff --git a/package-lock.json b/package-lock.json index e418cc4b3..81c993445 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9736,6 +9736,10 @@ "yaeti": "1.0.1" } }, + "libflac": { + "version": "git+https://github.com/ztl8702/libflac.git#31368097eaf9dcb5ef59365ef60b259cb7b97f07", + "from": "git+https://github.com/ztl8702/libflac.git#31368097eaf9dcb5ef59365ef60b259cb7b97f07" + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", diff --git a/package.json b/package.json index a2db2f112..cf771ccbe 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "jsc-android": "224109.1.0", "jwt-decode": "2.2.0", "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#e097a1189ed99838605d90b959e129155bc0e50a", + "libflac": "git+https://github.com/ztl8702/libflac.git#31368097eaf9dcb5ef59365ef60b259cb7b97f07", "lodash": "4.17.4", "moment": "2.19.4", "moment-duration-format": "2.2.2", diff --git a/react/features/local-recording/actionTypes.js b/react/features/local-recording/actionTypes.js new file mode 100644 index 000000000..5f002eab1 --- /dev/null +++ b/react/features/local-recording/actionTypes.js @@ -0,0 +1,40 @@ +/** + * Action to signal that the local client has started to perform recording, + * (as in: {@code RecordingAdapter} is actively collecting audio data). + * + * { + * type: LOCAL_RECORDING_ENGAGED + * } + */ +export const LOCAL_RECORDING_ENGAGED = Symbol('LOCAL_RECORDING_ENGAGED'); + +/** + * Action to signal that the local client has stopped recording, + * (as in: {@code RecordingAdapter} is no longer collecting audio data). + * + * { + * type: LOCAL_RECORDING_UNENGAGED + * } + */ +export const LOCAL_RECORDING_UNENGAGED = Symbol('LOCAL_RECORDING_UNENGAGED'); + +/** + * Action to show/hide {@code LocalRecordingInfoDialog}. + * + * { + * type: LOCAL_RECORDING_TOGGLE_DIALOG + * } + */ +export const LOCAL_RECORDING_TOGGLE_DIALOG + = Symbol('LOCAL_RECORDING_TOGGLE_DIALOG'); + +/** + * Action to update {@code LocalRecordingInfoDialog} with stats + * from all clients. + * + * { + * type: LOCAL_RECORDING_STATS_UPDATE + * } + */ +export const LOCAL_RECORDING_STATS_UPDATE + = Symbol('LOCAL_RECORDING_STATS_UPDATE'); diff --git a/react/features/local-recording/actions.js b/react/features/local-recording/actions.js new file mode 100644 index 000000000..9ed6f1c38 --- /dev/null +++ b/react/features/local-recording/actions.js @@ -0,0 +1,59 @@ +/* @flow */ + +import { + LOCAL_RECORDING_ENGAGED, + LOCAL_RECORDING_UNENGAGED, + LOCAL_RECORDING_TOGGLE_DIALOG, + LOCAL_RECORDING_STATS_UPDATE +} from './actionTypes'; + +/** + * Signals state change in local recording engagement. + * In other words, the events of the local WebWorker / MediaRecorder + * starting to record and finishing recording. + * + * Note that this is not the event fired when the users tries to start + * the recording in the UI. + * + * @param {bool} isEngaged - Whether local recording is engaged or not. + * @returns {{ + * type: LOCAL_RECORDING_ENGAGED + * }|{ + * type: LOCAL_RECORDING_UNENGAGED + * }} + */ +export function signalLocalRecordingEngagement(isEngaged: boolean) { + return { + type: isEngaged ? LOCAL_RECORDING_ENGAGED : LOCAL_RECORDING_UNENGAGED + }; +} + +/** + * Toggles the open/close state of {@code LocalRecordingInfoDialog}. + * + * @returns {{ + * type: LOCAL_RECORDING_TOGGLE_DIALOG + * }} + */ +export function toggleLocalRecordingInfoDialog() { + return { + type: LOCAL_RECORDING_TOGGLE_DIALOG + }; +} + +/** + * Updates the the local recording stats from each client, + * to be displayed on {@code LocalRecordingInfoDialog}. + * + * @param {*} stats - The stats object. + * @returns {{ + * type: LOCAL_RECORDING_STATS_UPDATE, + * stats: Object + * }} + */ +export function statsUpdate(stats: Object) { + return { + type: LOCAL_RECORDING_STATS_UPDATE, + stats + }; +} diff --git a/react/features/local-recording/components/LocalRecordingButton.js b/react/features/local-recording/components/LocalRecordingButton.js new file mode 100644 index 000000000..67d19f3ef --- /dev/null +++ b/react/features/local-recording/components/LocalRecordingButton.js @@ -0,0 +1,111 @@ +/* @flow */ + +import InlineDialog from '@atlaskit/inline-dialog'; +import React, { Component } from 'react'; + +import { translate } from '../../base/i18n'; +import { ToolbarButton } from '../../toolbox'; + +import LocalRecordingInfoDialog from './LocalRecordingInfoDialog'; + +/** + * The type of the React {@code Component} state of + * {@link LocalRecordingButton}. + */ +type Props = { + + /** + * Whether or not {@link LocalRecordingInfoDialog} should be displayed. + */ + isDialogShown: boolean, + + /** + * Callback function called when {@link LocalRecordingButton} is clicked. + */ + onClick: Function, + + /** + * Invoked to obtain translated strings. + */ + t: Function +} + +/** + * A React {@code Component} for opening or closing the + * {@code LocalRecordingInfoDialog}. + * + * @extends Component + */ +class LocalRecordingButton extends Component { + + /** + * Initializes a new {@code LocalRecordingButton} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props: Props) { + super(props); + + // Bind event handlers so they are only bound once per instance. + this._onClick = this._onClick.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { isDialogShown, t } = this.props; + const iconClasses + = `icon-thumb-menu ${isDialogShown + ? 'icon-rec toggled' : 'icon-rec'}`; + + return ( +
+ + } + isOpen = { isDialogShown } + onClose = { this._onCloseDialog } + position = { 'top right' }> + + +
+ ); + } + + _onClick: () => void; + + /** + * Callback invoked when the Toolbar button is clicked. + * + * @private + * @returns {void} + */ + _onClick() { + this.props.onClick(); + } + + _onCloseDialog: () => void; + + /** + * Callback invoked when {@code InlineDialog} signals that it should be + * close. + * + * @returns {void} + */ + _onCloseDialog() { + // Do nothing for now, because we want the dialog to stay open + // after certain time, otherwise the moderator might need to repeatly + // open the dialog to see the stats. + } +} + +export default translate(LocalRecordingButton); diff --git a/react/features/local-recording/components/LocalRecordingInfoDialog.js b/react/features/local-recording/components/LocalRecordingInfoDialog.js new file mode 100644 index 000000000..387cf56ce --- /dev/null +++ b/react/features/local-recording/components/LocalRecordingInfoDialog.js @@ -0,0 +1,332 @@ +/* @flow */ + +import moment from 'moment'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { translate } from '../../base/i18n'; +import { + PARTICIPANT_ROLE, + getLocalParticipant +} from '../../base/participants'; + +import { statsUpdate } from '../actions'; +import { recordingController } from '../controller'; + + +/** + * The type of the React {@code Component} props of + * {@link LocalRecordingInfoDialog}. + */ +type Props = { + + /** + * Redux store dispatch function. + */ + dispatch: Dispatch<*>, + + /** + * Current encoding format. + */ + encodingFormat: string, + + /** + * Whether the local user is the moderator. + */ + isModerator: boolean, + + /** + * Whether local recording is engaged. + */ + isOn: boolean, + + /** + * The start time of the current local recording session. + * Used to calculate the duration of recording. + */ + recordingStartedAt: Date, + + /** + * Stats of all the participant. + */ + stats: Object, + + /** + * Invoked to obtain translated strings. + */ + t: Function +} + +/** + * The type of the React {@code Component} state of + * {@link LocalRecordingInfoDialog}. + */ +type State = { + + /** + * The recording duration string to be displayed on the UI. + */ + durationString: string +} + +/** + * A React Component with the contents for a dialog that shows information about + * local recording. For users with moderator rights, this is also the "control + * panel" for starting/stopping local recording on all clients. + * + * @extends Component + */ +class LocalRecordingInfoDialog extends Component { + + /** + * Saves a handle to the timer for UI updates, + * so that it can be cancelled when the component unmounts. + */ + _timer: ?IntervalID; + + /** + * Constructor. + */ + constructor() { + super(); + this.state = { + durationString: 'N/A' + }; + } + + /** + * Implements React's {@link Component#componentWillMount()}. + * + * @returns {void} + */ + componentWillMount() { + this._timer = setInterval( + () => { + this.setState((_prevState, props) => { + const nowTime = new Date(Date.now()); + + return { + durationString: this._getDuration(nowTime, + props.recordingStartedAt) + }; + }); + try { + this.props.dispatch( + statsUpdate(recordingController + .getParticipantsStats())); + } catch (e) { + // do nothing + } + }, + 1000 + ); + } + + /** + * Implements React's {@link Component#componentWillUnmount()}. + * + * @returns {void} + */ + componentWillUnmount() { + if (this._timer) { + clearInterval(this._timer); + this._timer = null; + } + } + + + /** + * Returns React elements for displaying the local recording stats of + * each participant. + * + * @returns {ReactElement} + */ + renderStats() { + const { stats, t } = this.props; + + if (stats === undefined) { + return