This commit is contained in:
64
react/features/base/sounds/actionTypes.ts
Normal file
64
react/features/base/sounds/actionTypes.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* The type of a feature/internal/protected (redux) action to add an audio
|
||||
* element to the sounds collection state.
|
||||
*
|
||||
* {
|
||||
* type: _ADD_AUDIO_ELEMENT,
|
||||
* ref: AudioElement,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const _ADD_AUDIO_ELEMENT = '_ADD_AUDIO_ELEMENT';
|
||||
|
||||
/**
|
||||
* The type of feature/internal/protected (redux) action to remove an audio
|
||||
* element for given sound identifier from the sounds collection state.
|
||||
*
|
||||
* {
|
||||
* type: _REMOVE_AUDIO_ELEMENT,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const _REMOVE_AUDIO_ELEMENT = '_REMOVE_AUDIO_ELEMENT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to play a sound from the sounds collection.
|
||||
*
|
||||
* {
|
||||
* type: PLAY_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const PLAY_SOUND = 'PLAY_SOUND';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to register a new sound with the sounds
|
||||
* collection.
|
||||
*
|
||||
* {
|
||||
* type: REGISTER_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const REGISTER_SOUND = 'REGISTER_SOUND';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to stop a sound from the sounds collection.
|
||||
*
|
||||
* {
|
||||
* type: STOP_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const STOP_SOUND = 'STOP_SOUND';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to unregister an existing sound from the sounds
|
||||
* collection.
|
||||
*
|
||||
* {
|
||||
* type: UNREGISTER_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const UNREGISTER_SOUND = 'UNREGISTER_SOUND';
|
||||
155
react/features/base/sounds/actions.ts
Normal file
155
react/features/base/sounds/actions.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { getConferenceState } from '../conference/functions';
|
||||
import { Sounds } from '../config/configType';
|
||||
import { AudioElement } from '../media/components/AbstractAudio';
|
||||
|
||||
import {
|
||||
PLAY_SOUND,
|
||||
REGISTER_SOUND,
|
||||
STOP_SOUND,
|
||||
UNREGISTER_SOUND,
|
||||
_ADD_AUDIO_ELEMENT,
|
||||
_REMOVE_AUDIO_ELEMENT
|
||||
} from './actionTypes';
|
||||
import { getSoundsPath } from './functions';
|
||||
import { getDisabledSounds } from './functions.any';
|
||||
|
||||
/**
|
||||
* Adds {@link AudioElement} instance to the base/sounds feature state for the
|
||||
* {@link Sound} instance identified by the given id. After this action the
|
||||
* sound can be played by dispatching the {@link PLAY_SOUND} action.
|
||||
*
|
||||
* @param {string} soundId - The sound identifier for which the audio element
|
||||
* will be stored.
|
||||
* @param {AudioElement} audioElement - The audio element which implements the
|
||||
* audio playback functionality and which is backed by the sound resource
|
||||
* corresponding to the {@link Sound} with the given id.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* type: PLAY_SOUND,
|
||||
* audioElement: AudioElement,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function _addAudioElement(soundId: string, audioElement: AudioElement) {
|
||||
return {
|
||||
type: _ADD_AUDIO_ELEMENT,
|
||||
audioElement,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The opposite of {@link _addAudioElement} which removes {@link AudioElement}
|
||||
* for given sound from base/sounds state. It means that the audio resource has
|
||||
* been disposed and the sound can no longer be played.
|
||||
*
|
||||
* @param {string} soundId - The {@link Sound} instance identifier for which the
|
||||
* audio element is being removed.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* type: _REMOVE_AUDIO_ELEMENT,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function _removeAudioElement(soundId: string) {
|
||||
return {
|
||||
type: _REMOVE_AUDIO_ELEMENT,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the sound identified by the given sound id. The action
|
||||
* will have effect only if the audio resource has been loaded already.
|
||||
*
|
||||
* @param {string} soundId - The id of the sound to be played (the same one
|
||||
* which was used in {@link registerSound} to register the sound).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function playSound(soundId: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const disabledSounds = getDisabledSounds(state);
|
||||
const { leaving } = getConferenceState(state);
|
||||
|
||||
// Skip playing sounds when leaving, to avoid hearing that recording has stopped and so on.
|
||||
if (leaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!disabledSounds.includes(soundId as Sounds) && !disabledSounds.find(id => soundId.startsWith(id))) {
|
||||
dispatch({
|
||||
type: PLAY_SOUND,
|
||||
soundId
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new sound for given id and a source object which can be either a
|
||||
* path or a raw object depending on the platform (native vs web). It will make
|
||||
* the {@link SoundCollection} render extra HTMLAudioElement which will make it
|
||||
* available for playback through the {@link playSound} action.
|
||||
*
|
||||
* @param {string} soundId - The global identifier which identify the sound
|
||||
* created for given source object.
|
||||
* @param {string} soundName - The name of bundled audio file that will be
|
||||
* associated with the given {@code soundId}.
|
||||
* @param {Object} options - Optional parameters.
|
||||
* @param {boolean} options.loop - True in order to loop the sound.
|
||||
* @returns {{
|
||||
* type: REGISTER_SOUND,
|
||||
* soundId: string,
|
||||
* src: string,
|
||||
* options: {
|
||||
* loop: boolean
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function registerSound(
|
||||
soundId: string, soundName: string, options: Object = {}) {
|
||||
return {
|
||||
type: REGISTER_SOUND,
|
||||
soundId,
|
||||
src: `${getSoundsPath()}/${soundName}`,
|
||||
options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops playback of the sound identified by the given sound id.
|
||||
*
|
||||
* @param {string} soundId - The id of the sound to be stopped (the same one
|
||||
* which was used in {@link registerSound} to register the sound).
|
||||
* @returns {{
|
||||
* type: STOP_SOUND,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function stopSound(soundId: string) {
|
||||
return {
|
||||
type: STOP_SOUND,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the sound identified by the given id. It will make the
|
||||
* {@link SoundCollection} component stop rendering the corresponding
|
||||
* {@code HTMLAudioElement} which then should result in the audio resource
|
||||
* disposal.
|
||||
*
|
||||
* @param {string} soundId - The identifier of the {@link Sound} to be removed.
|
||||
* @returns {{
|
||||
* type: UNREGISTER_SOUND,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function unregisterSound(soundId: string) {
|
||||
return {
|
||||
type: UNREGISTER_SOUND,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
154
react/features/base/sounds/components/SoundCollection.ts
Normal file
154
react/features/base/sounds/components/SoundCollection.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { AudioElement } from '../../media/components/AbstractAudio';
|
||||
import { Audio } from '../../media/components/index';
|
||||
import { _addAudioElement, _removeAudioElement } from '../actions';
|
||||
import { Sound } from '../reducer';
|
||||
|
||||
/**
|
||||
* {@link SoundCollection}'s properties.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Dispatches {@link _ADD_AUDIO_ELEMENT} Redux action which will store the
|
||||
* {@link AudioElement} for a sound in the Redux store.
|
||||
*/
|
||||
_addAudioElement: Function;
|
||||
|
||||
/**
|
||||
* Dispatches {@link _REMOVE_AUDIO_ELEMENT} Redux action which will remove
|
||||
* the sound's {@link AudioElement} from the Redux store.
|
||||
*/
|
||||
_removeAudioElement: Function;
|
||||
|
||||
/**
|
||||
* It's the 'base/sounds' reducer's state mapped to a property. It's used to
|
||||
* render audio elements for every registered sound.
|
||||
*/
|
||||
_sounds: Map<string, Sound>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collections of all global sounds used by the app for playing audio
|
||||
* notifications in response to various events. It renders <code>Audio</code>
|
||||
* element for each sound registered in the base/sounds feature. When the audio
|
||||
* resource is loaded it will emit add/remove audio element actions which will
|
||||
* attach the element to the corresponding {@link Sound} instance in the Redux
|
||||
* state. When that happens the sound can be played using the {@link playSound}
|
||||
* action.
|
||||
*/
|
||||
class SoundCollection extends Component<IProps> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
let key = 0;
|
||||
const sounds = [];
|
||||
|
||||
for (const [ soundId, sound ] of this.props._sounds.entries()) {
|
||||
const { options, src } = sound;
|
||||
|
||||
sounds.push(
|
||||
React.createElement(
|
||||
Audio, {
|
||||
key,
|
||||
setRef: this._setRef.bind(this, soundId),
|
||||
src,
|
||||
loop: options?.loop
|
||||
}));
|
||||
key += 1;
|
||||
}
|
||||
|
||||
return sounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the (reference to the) {@link AudioElement} object which implements
|
||||
* the audio playback functionality.
|
||||
*
|
||||
* @param {string} soundId - The sound Id for the audio element for which
|
||||
* the callback is being executed.
|
||||
* @param {AudioElement} element - The {@link AudioElement} instance
|
||||
* which implements the audio playback functionality.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_setRef(soundId: string, element?: AudioElement) {
|
||||
if (element) {
|
||||
this.props._addAudioElement(soundId, element);
|
||||
} else {
|
||||
this.props._removeAudioElement(soundId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to {@code SoundCollection}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _sounds: Map<string, Sound>
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_sounds: state['features/base/sounds']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps dispatching of some actions to React component props.
|
||||
*
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _addAudioElement: void,
|
||||
* _removeAudioElement: void
|
||||
* }}
|
||||
*/
|
||||
export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
return {
|
||||
/**
|
||||
* Dispatches action to store the {@link AudioElement} for
|
||||
* a {@link Sound} identified by given <tt>soundId</tt> in the Redux
|
||||
* store, so that the playback can be controlled through the Redux
|
||||
* actions.
|
||||
*
|
||||
* @param {string} soundId - A global identifier which will be used to
|
||||
* identify the {@link Sound} instance for which an audio element will
|
||||
* be added.
|
||||
* @param {AudioElement} audioElement - The {@link AudioElement}
|
||||
* instance that will be stored in the Redux state of the base/sounds
|
||||
* feature, as part of the {@link Sound} object. At that point the sound
|
||||
* will be ready for playback.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_addAudioElement(soundId: string, audioElement: AudioElement) {
|
||||
dispatch(_addAudioElement(soundId, audioElement));
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches action to remove {@link AudioElement} from the Redux
|
||||
* store for specific {@link Sound}, because it is no longer part of
|
||||
* the DOM tree and the audio resource will be released.
|
||||
*
|
||||
* @param {string} soundId - The id of the {@link Sound} instance for
|
||||
* which an {@link AudioElement} will be removed from the Redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_removeAudioElement(soundId: string) {
|
||||
dispatch(_removeAudioElement(soundId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(SoundCollection);
|
||||
9
react/features/base/sounds/functions.android.ts
Normal file
9
react/features/base/sounds/functions.android.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Returns the location of the sounds. On Android sounds files are copied to
|
||||
* the 'assets/sounds/' folder of the SDK bundle on build time.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getSoundsPath() {
|
||||
return 'asset:/sounds';
|
||||
}
|
||||
11
react/features/base/sounds/functions.any.ts
Normal file
11
react/features/base/sounds/functions.any.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
/**
|
||||
* Selector for retrieving the disabled sounds array.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Array<string>} - The disabled sound id's array.
|
||||
*/
|
||||
export function getDisabledSounds(state: IReduxState) {
|
||||
return state['features/base/config'].disabledSounds || [];
|
||||
}
|
||||
12
react/features/base/sounds/functions.ios.ts
Normal file
12
react/features/base/sounds/functions.ios.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getSdkBundlePath } from '../../app/functions.native';
|
||||
|
||||
/**
|
||||
* Returns the location of the sounds. On iOS it's the location of the SDK
|
||||
* bundle on the phone. Each sound file must be added to the SDK's XCode project
|
||||
* in order to be bundled correctly.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getSoundsPath() {
|
||||
return getSdkBundlePath();
|
||||
}
|
||||
27
react/features/base/sounds/functions.web.ts
Normal file
27
react/features/base/sounds/functions.web.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { IStore } from '../../app/types';
|
||||
|
||||
/**
|
||||
* Returns the location of the sounds. On Web it's the relative path to
|
||||
* the sounds folder placed in the source root.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getSoundsPath() {
|
||||
return 'sounds';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new audio output device on the global sound elements.
|
||||
*
|
||||
* @param {string } deviceId - The new output deviceId.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setNewAudioOutputDevice(deviceId: string) {
|
||||
return function(_dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const sounds = getState()['features/base/sounds'];
|
||||
|
||||
for (const [ , sound ] of sounds) {
|
||||
sound.audioElement?.setSinkId?.(deviceId);
|
||||
}
|
||||
};
|
||||
}
|
||||
3
react/features/base/sounds/logger.ts
Normal file
3
react/features/base/sounds/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/sounds');
|
||||
129
react/features/base/sounds/middleware.any.ts
Normal file
129
react/features/base/sounds/middleware.any.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { registerE2eeAudioFiles } from '../../../features/e2ee/functions';
|
||||
import { registerRecordingAudioFiles } from '../../../features/recording/functions';
|
||||
import { IStore } from '../../app/types';
|
||||
import { AudioSupportedLanguage } from '../media/constants';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
import { PLAY_SOUND, STOP_SOUND } from './actionTypes';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Implements the entry point of the middleware of the feature base/sounds.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case PLAY_SOUND:
|
||||
_playSound(store, action.soundId);
|
||||
break;
|
||||
case STOP_SOUND:
|
||||
_stopSound(store, action.soundId);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Plays sound from audio element registered in the Redux store.
|
||||
*
|
||||
* @param {Store} store - The Redux store instance.
|
||||
* @param {string} soundId - Audio element identifier.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _playSound({ getState }: IStore, soundId: string) {
|
||||
const sounds = getState()['features/base/sounds'];
|
||||
const sound = sounds.get(soundId);
|
||||
|
||||
if (sound) {
|
||||
if (sound.audioElement) {
|
||||
sound.audioElement.play();
|
||||
} else {
|
||||
logger.warn(`PLAY_SOUND: sound not loaded yet for id: ${soundId}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`PLAY_SOUND: no sound found for id: ${soundId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop sound from audio element registered in the Redux store.
|
||||
*
|
||||
* @param {Store} store - The Redux store instance.
|
||||
* @param {string} soundId - Audio element identifier.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _stopSound({ getState }: IStore, soundId: string) {
|
||||
const sounds = getState()['features/base/sounds'];
|
||||
const sound = sounds.get(soundId);
|
||||
|
||||
if (sound) {
|
||||
const { audioElement } = sound;
|
||||
|
||||
if (audioElement) {
|
||||
audioElement.stop();
|
||||
} else {
|
||||
logger.warn(`STOP_SOUND: sound not loaded yet for id: ${soundId}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`STOP_SOUND: no sound found for id: ${soundId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the language is supported for audio messages.
|
||||
*
|
||||
* @param {string} language - The requested language.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isLanguageSupported(language: string): Boolean {
|
||||
return Boolean(AudioSupportedLanguage[language as keyof typeof AudioSupportedLanguage]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking if it's necessary to reload the translated files.
|
||||
*
|
||||
* @param {string} language - The next language.
|
||||
* @param {string} prevLanguage - The previous language.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldReloadAudioFiles(language: string, prevLanguage: string): Boolean {
|
||||
const isNextLanguageSupported = isLanguageSupported(language);
|
||||
const isPrevLanguageSupported = isLanguageSupported(prevLanguage);
|
||||
|
||||
return (
|
||||
|
||||
// From an unsupported language (which defaulted to English) to a supported language (that isn't English).
|
||||
isNextLanguageSupported && language !== AudioSupportedLanguage.en && !isPrevLanguageSupported
|
||||
) || (
|
||||
|
||||
// From a supported language (that wasn't English) to English.
|
||||
!isNextLanguageSupported && isPrevLanguageSupported && prevLanguage !== AudioSupportedLanguage.en
|
||||
) || (
|
||||
|
||||
// From a supported language to another.
|
||||
isNextLanguageSupported && isPrevLanguageSupported
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up state change listener for language.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
() => i18next.language,
|
||||
(language, { dispatch }, prevLanguage): void => {
|
||||
|
||||
if (language !== prevLanguage && shouldReloadAudioFiles(language, prevLanguage)) {
|
||||
registerE2eeAudioFiles(dispatch, true);
|
||||
registerRecordingAudioFiles(dispatch, true);
|
||||
}
|
||||
}
|
||||
);
|
||||
1
react/features/base/sounds/middleware.native.ts
Normal file
1
react/features/base/sounds/middleware.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './middleware.any';
|
||||
23
react/features/base/sounds/middleware.web.ts
Normal file
23
react/features/base/sounds/middleware.web.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getAudioOutputDeviceId } from '../devices/functions.web';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
|
||||
import { _ADD_AUDIO_ELEMENT } from './actionTypes';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
/**
|
||||
* Implements the entry point of the middleware of the feature base/sounds.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(_store => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case _ADD_AUDIO_ELEMENT:
|
||||
action.audioElement?.setSinkId?.(getAudioOutputDeviceId());
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
144
react/features/base/sounds/reducer.ts
Normal file
144
react/features/base/sounds/reducer.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { AudioElement } from '../media/components/AbstractAudio';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { assign } from '../redux/functions';
|
||||
|
||||
import {
|
||||
REGISTER_SOUND,
|
||||
UNREGISTER_SOUND,
|
||||
_ADD_AUDIO_ELEMENT,
|
||||
_REMOVE_AUDIO_ELEMENT
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* The structure use by this reducer to describe a sound.
|
||||
*/
|
||||
export type Sound = {
|
||||
|
||||
/**
|
||||
* The HTMLAudioElement which implements the audio playback functionality.
|
||||
* Becomes available once the sound resource gets loaded and the sound can
|
||||
* not be played until that happens.
|
||||
*/
|
||||
audioElement?: AudioElement;
|
||||
|
||||
/**
|
||||
* This field is container for all optional parameters related to the sound.
|
||||
*/
|
||||
options?: {
|
||||
loop: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* This field describes the source of the audio resource to be played. It
|
||||
* can be either a path to the file or an object depending on the platform
|
||||
* (native vs web).
|
||||
*/
|
||||
src?: Object | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial/default state of the feature {@code base/sounds}. It is a {@code Map}
|
||||
* of globally stored sounds.
|
||||
*
|
||||
* @type {Map<string, Sound>}
|
||||
*/
|
||||
const DEFAULT_STATE = new Map();
|
||||
|
||||
export type ISoundsState = Map<string, Sound>;
|
||||
|
||||
/**
|
||||
* The base/sounds feature's reducer.
|
||||
*/
|
||||
ReducerRegistry.register<ISoundsState>(
|
||||
'features/base/sounds',
|
||||
(state = DEFAULT_STATE, action): ISoundsState => {
|
||||
switch (action.type) {
|
||||
case _ADD_AUDIO_ELEMENT:
|
||||
case _REMOVE_AUDIO_ELEMENT:
|
||||
return _addOrRemoveAudioElement(state, action);
|
||||
|
||||
case REGISTER_SOUND:
|
||||
return _registerSound(state, action);
|
||||
|
||||
case UNREGISTER_SOUND:
|
||||
return _unregisterSound(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds or removes {@link AudioElement} associated with a {@link Sound}.
|
||||
*
|
||||
* @param {Map<string, Sound>} state - The current Redux state of this feature.
|
||||
* @param {_ADD_AUDIO_ELEMENT | _REMOVE_AUDIO_ELEMENT} action - The action to be
|
||||
* handled.
|
||||
* @private
|
||||
* @returns {Map<string, Sound>}
|
||||
*/
|
||||
function _addOrRemoveAudioElement(state: ISoundsState, action: AnyAction) {
|
||||
const isAddAction = action.type === _ADD_AUDIO_ELEMENT;
|
||||
const nextState = new Map(state);
|
||||
const { soundId } = action;
|
||||
|
||||
const sound = nextState.get(soundId);
|
||||
|
||||
if (sound) {
|
||||
if (isAddAction) {
|
||||
nextState.set(soundId,
|
||||
assign(sound, {
|
||||
audioElement: action.audioElement
|
||||
}));
|
||||
} else {
|
||||
nextState.set(soundId,
|
||||
assign(sound, {
|
||||
audioElement: undefined
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new {@link Sound} for given id and source. It will make
|
||||
* the {@link SoundCollection} component render HTMLAudioElement for given
|
||||
* source making it available for playback through the redux actions.
|
||||
*
|
||||
* @param {Map<string, Sound>} state - The current Redux state of the sounds
|
||||
* features.
|
||||
* @param {REGISTER_SOUND} action - The register sound action.
|
||||
* @private
|
||||
* @returns {Map<string, Sound>}
|
||||
*/
|
||||
function _registerSound(state: ISoundsState, action: AnyAction) {
|
||||
const nextState = new Map(state);
|
||||
|
||||
nextState.set(action.soundId, {
|
||||
src: action.src,
|
||||
options: action.options
|
||||
});
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a {@link Sound} which will make the {@link SoundCollection}
|
||||
* component stop rendering the corresponding HTMLAudioElement. This will
|
||||
* result further in the audio resource disposal.
|
||||
*
|
||||
* @param {Map<string, Sound>} state - The current Redux state of this feature.
|
||||
* @param {UNREGISTER_SOUND} action - The unregister sound action.
|
||||
* @private
|
||||
* @returns {Map<string, Sound>}
|
||||
*/
|
||||
function _unregisterSound(state: ISoundsState, action: AnyAction) {
|
||||
const nextState = new Map(state);
|
||||
|
||||
nextState.delete(action.soundId);
|
||||
|
||||
return nextState;
|
||||
}
|
||||
Reference in New Issue
Block a user