浏览代码

feat: add join/leave sounds on mobile

Adds base/sounds feature which allows other features to register a sound
source under specified id. A new SoundsCollection component will then
render corresponding HTMLAudioElement for each such sound. Once "setRef"
callback is called by the HTMLAudioElement, this element will be added
to the Redux store. When that happens sound can be played through the
new 'playSound' action which will call play() method on the stored
HTMLAudioElement instance.
master
paweldomas 7 年前
父节点
当前提交
60e03e3dec

+ 1
- 0
android/sdk/build.gradle 查看文件

@@ -29,6 +29,7 @@ dependencies {
29 29
     compile project(':react-native-immersive')
30 30
     compile project(':react-native-keep-awake')
31 31
     compile project(':react-native-locale-detector')
32
+    compile project(':react-native-sound')
32 33
     compile project(':react-native-vector-icons')
33 34
     compile project(':react-native-webrtc')
34 35
 }

+ 1
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java 查看文件

@@ -122,6 +122,7 @@ public class JitsiMeetView extends FrameLayout {
122 122
                 .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
123 123
                 .addPackage(new com.RNFetchBlob.RNFetchBlobPackage())
124 124
                 .addPackage(new com.rnimmersive.RNImmersivePackage())
125
+                .addPackage(new com.zmxv.RNSound.RNSoundPackage())
125 126
                 .addPackage(new ReactPackageAdapter() {
126 127
                     @Override
127 128
                     public List<NativeModule> createNativeModules(

+ 2
- 0
android/settings.gradle 查看文件

@@ -11,6 +11,8 @@ include ':react-native-keep-awake'
11 11
 project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
12 12
 include ':react-native-locale-detector'
13 13
 project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android')
14
+include ':react-native-sound'
15
+project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
14 16
 include ':react-native-vector-icons'
15 17
 project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
16 18
 include ':react-native-webrtc'

+ 1
- 0
ios/Podfile 查看文件

@@ -28,6 +28,7 @@ target 'JitsiMeet' do
28 28
   pod 'react-native-locale-detector',
29 29
     :path => '../node_modules/react-native-locale-detector'
30 30
   pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
31
+  pod 'RNSound', :path => '../node_modules/react-native-sound'
31 32
   pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
32 33
 end
33 34
 

+ 10
- 1
ios/Podfile.lock 查看文件

@@ -41,6 +41,11 @@ PODS:
41 41
     - React/Core
42 42
     - React/fishhook
43 43
     - React/RCTBlob
44
+  - RNSound (0.10.4):
45
+    - React/Core
46
+    - RNSound/Core (= 0.10.4)
47
+  - RNSound/Core (0.10.4):
48
+    - React/Core
44 49
   - RNVectorIcons (4.4.2):
45 50
     - React
46 51
   - yoga (0.51.0.React)
@@ -61,6 +66,7 @@ DEPENDENCIES:
61 66
   - React/RCTNetwork (from `../node_modules/react-native`)
62 67
   - React/RCTText (from `../node_modules/react-native`)
63 68
   - React/RCTWebSocket (from `../node_modules/react-native`)
69
+  - RNSound (from `../node_modules/react-native-sound`)
64 70
   - RNVectorIcons (from `../node_modules/react-native-vector-icons`)
65 71
   - yoga (from `../node_modules/react-native/ReactCommon/yoga`)
66 72
 
@@ -77,6 +83,8 @@ EXTERNAL SOURCES:
77 83
     :path: ../node_modules/react-native-locale-detector
78 84
   react-native-webrtc:
79 85
     :path: ../node_modules/react-native-webrtc
86
+  RNSound:
87
+    :path: ../node_modules/react-native-sound
80 88
   RNVectorIcons:
81 89
     :path: ../node_modules/react-native-vector-icons
82 90
   yoga:
@@ -89,9 +97,10 @@ SPEC CHECKSUMS:
89 97
   react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
90 98
   react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
91 99
   react-native-webrtc: bc044ca9530fc802e7533f247aa08fe1b6bf8dc5
100
+  RNSound: d0818fe2435254fe30540fae48a429c5ffb72e09
92 101
   RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
93 102
   yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
94 103
 
95
-PODFILE CHECKSUM: fabd6b6c27f8e1849f0668db3f403bf536ac8903
104
+PODFILE CHECKSUM: 1e6ce4da1b385720c726f3f131a6aaf08bf9c0ba
96 105
 
97 106
 COCOAPODS: 1.4.0

+ 0
- 10
modules/UI/UI.js 查看文件

@@ -503,11 +503,6 @@ UI.addUser = function(user) {
503 503
         APP.store.dispatch(showParticipantJoinedNotification(displayName));
504 504
     }
505 505
 
506
-    if (!config.startAudioMuted
507
-        || config.startAudioMuted > APP.conference.membersCount) {
508
-        UIUtil.playSoundNotification('userJoined');
509
-    }
510
-
511 506
     // Add Peer's container
512 507
     VideoLayout.addParticipantContainer(user);
513 508
 
@@ -529,11 +524,6 @@ UI.removeUser = function(id, displayName) {
529 524
     messageHandler.participantNotification(
530 525
         displayName, 'notify.somebody', 'disconnected', 'notify.disconnected');
531 526
 
532
-    if (!config.startAudioMuted
533
-            || config.startAudioMuted > APP.conference.membersCount) {
534
-        UIUtil.playSoundNotification('userLeft');
535
-    }
536
-
537 527
     VideoLayout.removeParticipantContainer(id);
538 528
 };
539 529
 

+ 5
- 0
package-lock.json 查看文件

@@ -9905,6 +9905,11 @@
9905 9905
       "resolved": "https://registry.npmjs.org/react-native-prompt/-/react-native-prompt-1.0.0.tgz",
9906 9906
       "integrity": "sha1-QeDsKqfdjxLzo+6Dr51jxLZw+KE="
9907 9907
     },
9908
+    "react-native-sound": {
9909
+      "version": "0.10.4",
9910
+      "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.4.tgz",
9911
+      "integrity": "sha512-V9v4CjKgv8ekQRLOJSoKA7pxJ03F4Ih3T/RfMIlMWLktz7v/O4sdJPjRBLOzZRqAnr9FWTLbSk1ZCjioXh3mjQ=="
9912
+    },
9908 9913
     "react-native-vector-icons": {
9909 9914
       "version": "4.4.2",
9910 9915
       "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-4.4.2.tgz",

+ 1
- 0
package.json 查看文件

@@ -62,6 +62,7 @@
62 62
     "react-native-keep-awake": "2.0.6",
63 63
     "react-native-locale-detector": "github:jitsi/react-native-locale-detector#cc76092fc4335488a28a9529c8b50afae2c3ecdc",
64 64
     "react-native-prompt": "1.0.0",
65
+    "react-native-sound": "0.10.4",
65 66
     "react-native-vector-icons": "4.4.2",
66 67
     "react-native-webrtc": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
67 68
     "react-redux": "5.0.6",

+ 3
- 1
react/features/app/components/AbstractApp.js 查看文件

@@ -15,6 +15,7 @@ import {
15 15
 import '../../base/profile';
16 16
 import { Fragment, RouteRegistry } from '../../base/react';
17 17
 import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
18
+import { SoundCollection } from '../../base/sounds';
18 19
 import { PersistenceRegistry } from '../../base/storage';
19 20
 import { toURLString } from '../../base/util';
20 21
 import { OverlayContainer } from '../../overlay';
@@ -274,6 +275,7 @@ export class AbstractApp extends Component {
274 275
                     <Provider store = { this._getStore() }>
275 276
                         <Fragment>
276 277
                             { this._createElement(component) }
278
+                            <SoundCollection />
277 279
                             <OverlayContainer />
278 280
                         </Fragment>
279 281
                     </Provider>
@@ -501,7 +503,7 @@ export class AbstractApp extends Component {
501 503
     /**
502 504
      * Navigates this {@code AbstractApp} to (i.e. opens) a specific URL.
503 505
      *
504
-     * @param {string|Object} url - The URL to navigate this {@code AbstractApp}
506
+     * @param {Object|string} url - The URL to navigate this {@code AbstractApp}
505 507
      * to (i.e. the URL to open).
506 508
      * @protected
507 509
      * @returns {void}

+ 1
- 1
react/features/base/jwt/components/CalleeInfo.js 查看文件

@@ -254,7 +254,7 @@ class CalleeInfo extends Component<Props, State> {
254 254
         if (this.state.renderAudio && this.state.ringing) {
255 255
             return (
256 256
                 <Audio
257
-                    ref = { this._setAudio }
257
+                    setRef = { this._setAudio }
258 258
                     src = './sounds/ring.ogg' />
259 259
             );
260 260
         }

+ 52
- 66
react/features/base/media/components/AbstractAudio.js 查看文件

@@ -1,35 +1,54 @@
1 1
 // @flow
2 2
 
3
-import PropTypes from 'prop-types';
4
-import React, { Component } from 'react';
3
+import { Component } from 'react';
4
+
5
+/**
6
+ * Describes audio element interface used in the base/media feature for audio
7
+ * playback.
8
+ */
9
+export type AudioElement = {
10
+    play: Function,
11
+    pause: Function
12
+}
13
+
14
+/**
15
+ * {@code AbstractAudio} component's property types.
16
+ */
17
+type Props = {
18
+
19
+    /**
20
+     * A callback which will be called with {@code AbstractAudio} instance once
21
+     * the audio element is loaded.
22
+     */
23
+    setRef: ?Function,
24
+
25
+    /**
26
+     * The URL of a media resource to use in the element.
27
+     *
28
+     * NOTE on react-native sound files are imported through 'require' and then
29
+     * passed as the 'src' parameter which means their type will be 'any'.
30
+     *
31
+     * @type {Object | string}
32
+     */
33
+    src: Object | string,
34
+    stream: Object
35
+}
5 36
 
6 37
 /**
7 38
  * The React {@link Component} which is similar to Web's
8 39
  * {@code HTMLAudioElement}.
9 40
  */
10
-export default class AbstractAudio extends Component<*> {
41
+export default class AbstractAudio extends Component<Props> {
11 42
     /**
12
-     * The (reference to the) {@link ReactElement} which actually implements
13
-     * this {@code AbstractAudio}.
43
+     * The {@link AudioElement} instance which implements the audio playback
44
+     * functionality.
14 45
      */
15
-    _ref: ?Object;
16
-
17
-    _setRef: Function;
46
+    _audioElementImpl: ?AudioElement;
18 47
 
19 48
     /**
20
-     * {@code AbstractAudio} component's property types.
21
-     *
22
-     * @static
49
+     * {@link setAudioElementImpl} bound to <code>this</code>.
23 50
      */
24
-    static propTypes = {
25
-        /**
26
-         * The URL of a media resource to use in the element.
27
-         *
28
-         * @type {string}
29
-         */
30
-        src: PropTypes.string,
31
-        stream: PropTypes.object
32
-    };
51
+    setAudioElementImpl: Function;
33 52
 
34 53
     /**
35 54
      * Initializes a new {@code AbstractAudio} instance.
@@ -41,7 +60,7 @@ export default class AbstractAudio extends Component<*> {
41 60
         super(props);
42 61
 
43 62
         // Bind event handlers so they are only bound once for every instance.
44
-        this._setRef = this._setRef.bind(this);
63
+        this.setAudioElementImpl = this.setAudioElementImpl.bind(this);
45 64
     }
46 65
 
47 66
     /**
@@ -51,7 +70,7 @@ export default class AbstractAudio extends Component<*> {
51 70
      * @returns {void}
52 71
      */
53 72
     pause() {
54
-        this._ref && typeof this._ref.pause === 'function' && this._ref.pause();
73
+        this._audioElementImpl && this._audioElementImpl.pause();
55 74
     }
56 75
 
57 76
     /**
@@ -61,56 +80,23 @@ export default class AbstractAudio extends Component<*> {
61 80
      * @returns {void}
62 81
      */
63 82
     play() {
64
-        this._ref && typeof this._ref.play === 'function' && this._ref.play();
83
+        this._audioElementImpl && this._audioElementImpl.play();
65 84
     }
66 85
 
67 86
     /**
68
-     * Renders this {@code AbstractAudio} as a React {@link Component} of a
69
-     * specific type.
87
+     * Set the (reference to the) {@link AudioElement} object which implements
88
+     * the audio playback functionality.
70 89
      *
71
-     * @param {string|ReactClass} type - The type of the React {@code Component}
72
-     * which is to be rendered.
73
-     * @param {Object|undefined} props - The read-only React {@code Component}
74
-     * properties, if any, to render. If {@code undefined}, the props of this
75
-     * instance will be rendered.
90
+     * @param {AudioElement} element - The {@link AudioElement} instance
91
+     * which implements the audio playback functionality.
76 92
      * @protected
77
-     * @returns {ReactElement}
78
-     */
79
-    _render(type, props) {
80
-        const {
81
-            children,
82
-
83
-            /* eslint-disable no-unused-vars */
84
-
85
-            // The following properties are consumed by React itself so they are
86
-            // to not be propagated.
87
-            ref,
88
-
89
-            /* eslint-enable no-unused-vars */
90
-
91
-            ...filteredProps
92
-        } = props || this.props;
93
-
94
-        return (
95
-            React.createElement(
96
-                type,
97
-                {
98
-                    ...filteredProps,
99
-                    ref: this._setRef
100
-                },
101
-                children));
102
-    }
103
-
104
-    /**
105
-     * Set the (reference to the) {@link ReactElement} which actually implements
106
-     * this {@code AbstractAudio}.
107
-     *
108
-     * @param {Object} ref - The (reference to the) {@code ReactElement} which
109
-     * actually implements this {@code AbstractAudio}.
110
-     * @private
111 93
      * @returns {void}
112 94
      */
113
-    _setRef(ref) {
114
-        this._ref = ref;
95
+    setAudioElementImpl(element: ?AudioElement) {
96
+        this._audioElementImpl = element;
97
+
98
+        if (typeof this.props.setRef === 'function') {
99
+            this.props.setRef(element ? this : null);
100
+        }
115 101
     }
116 102
 }

+ 50
- 3
react/features/base/media/components/native/Audio.js 查看文件

@@ -1,19 +1,66 @@
1 1
 /* @flow */
2 2
 
3
+import Sound from 'react-native-sound';
4
+
3 5
 import AbstractAudio from '../AbstractAudio';
4 6
 
7
+const logger = require('jitsi-meet-logger').getLogger(__filename);
8
+
5 9
 /**
6 10
  * The React Native/mobile {@link Component} which is similar to Web's
7 11
  * {@code HTMLAudioElement} and wraps around react-native-webrtc's
8 12
  * {@link RTCView}.
9 13
  */
10 14
 export default class Audio extends AbstractAudio {
15
+
11 16
     /**
12
-     * {@code Audio} component's property types.
17
+     * Reference to 'react-native-sound} {@link Sound} instance.
18
+     */
19
+    _sound: Sound
20
+
21
+    /**
22
+     * A callback passed to the 'react-native-sound''s {@link Sound} instance,
23
+     * called when loading sound is finished.
13 24
      *
14
-     * @static
25
+     * @param {Object} error - The error object passed by
26
+     * the 'react-native-sound' library.
27
+     * @returns {void}
28
+     * @private
15 29
      */
16
-    static propTypes = AbstractAudio.propTypes;
30
+    _soundLoadedCallback(error) {
31
+        if (error) {
32
+            logger.error('Failed to load sound', error);
33
+        } else {
34
+            this.setAudioElementImpl(this._sound);
35
+        }
36
+    }
37
+
38
+    /**
39
+     * Will load the sound, after the component did mount.
40
+     *
41
+     * @returns {void}
42
+     */
43
+    componentDidMount() {
44
+        this._sound
45
+            = this.props.src
46
+                ? new Sound(
47
+                    this.props.src,
48
+                    this._soundLoadedCallback.bind(this))
49
+                : null;
50
+    }
51
+
52
+    /**
53
+     * Will dispose sound resources (if any) when component is about to unmount.
54
+     *
55
+     * @returns {void}
56
+     */
57
+    componentWillUnmount() {
58
+        if (this._sound) {
59
+            this.setAudioElementImpl(null);
60
+            this._sound.release();
61
+            this._sound = null;
62
+        }
63
+    }
17 64
 
18 65
     /**
19 66
      * Implements React's {@link Component#render()}.

+ 87
- 4
react/features/base/media/components/web/Audio.js 查看文件

@@ -1,5 +1,7 @@
1 1
 /* @flow */
2 2
 
3
+import React from 'react';
4
+
3 5
 import AbstractAudio from '../AbstractAudio';
4 6
 
5 7
 /**
@@ -8,11 +10,38 @@ import AbstractAudio from '../AbstractAudio';
8 10
  */
9 11
 export default class Audio extends AbstractAudio {
10 12
     /**
11
-     * {@code Audio} component's property types.
13
+     * Set to <code>true</code> when the whole file is loaded.
14
+     */
15
+    _audioFileLoaded: boolean;
16
+
17
+    /**
18
+     * {@link _onCanPlayThrough} bound to "this".
19
+     */
20
+    _onCanPlayThrough: Function;
21
+
22
+    /**
23
+     * Reference to the HTML audio element, stored until the file is ready.
24
+     */
25
+    _ref: HTMLAudioElement;
26
+
27
+    /**
28
+     * {@link _setRef} bound to "this".
29
+     */
30
+    _setRef: Function;
31
+
32
+    /**
33
+     * Creates new <code>Audio</code> element instance with given props.
12 34
      *
13
-     * @static
35
+     * @param {Object} props - The read-only properties with which the new
36
+     * instance is to be initialized.
14 37
      */
15
-    static propTypes = AbstractAudio.propTypes;
38
+    constructor(props: Object) {
39
+        super(props);
40
+
41
+        // Bind event handlers so they are only bound once for every instance.
42
+        this._onCanPlayThrough = this._onCanPlayThrough.bind(this);
43
+        this._setRef = this._setRef.bind(this);
44
+    }
16 45
 
17 46
     /**
18 47
      * Implements React's {@link Component#render()}.
@@ -21,6 +50,60 @@ export default class Audio extends AbstractAudio {
21 50
      * @returns {ReactElement}
22 51
      */
23 52
     render() {
24
-        return super._render('audio');
53
+        return (
54
+            <audio
55
+                onCanPlayThrough = { this._onCanPlayThrough }
56
+                preload = 'auto'
57
+                ref = { this._setRef }
58
+                src = { this.props.src } />
59
+        );
60
+    }
61
+
62
+    /**
63
+     * If audio element reference has been set and the file has been
64
+     * loaded then {@link setAudioElementImpl} will be called to eventually add
65
+     * the audio to the Redux store.
66
+     *
67
+     * @private
68
+     * @returns {void}
69
+     */
70
+    _maybeSetAudioElementImpl() {
71
+        if (this._ref && this._audioFileLoaded) {
72
+            this.setAudioElementImpl(this._ref);
73
+        }
74
+    }
75
+
76
+    /**
77
+     * Called when 'canplaythrough' event is triggered on the audio element,
78
+     * which means that the whole file has been loaded.
79
+     *
80
+     * @private
81
+     * @returns {void}
82
+     */
83
+    _onCanPlayThrough() {
84
+        this._audioFileLoaded = true;
85
+        this._maybeSetAudioElementImpl();
86
+    }
87
+
88
+    /**
89
+     * Sets the reference to the HTML audio element.
90
+     *
91
+     * @param {HTMLAudioElement} audioElement - The HTML audio element instance.
92
+     * @private
93
+     * @returns {void}
94
+     */
95
+    _setRef(audioElement: HTMLAudioElement) {
96
+        this._ref = audioElement;
97
+        if (audioElement) {
98
+            this._maybeSetAudioElementImpl();
99
+        } else {
100
+            // AbstractAudioElement is supposed to trigger "removeAudio" only if
101
+            // it was previously added, so it's safe to just call it.
102
+            this.setAudioElementImpl(null);
103
+
104
+            // Reset the loaded flag, as the audio element is being removed from
105
+            // the DOM tree.
106
+            this._audioFileLoaded = false;
107
+        }
25 108
     }
26 109
 }

+ 14
- 0
react/features/base/participants/constants.js 查看文件

@@ -28,6 +28,20 @@ export const LOCAL_PARTICIPANT_DEFAULT_ID = 'local';
28 28
  */
29 29
 export const MAX_DISPLAY_NAME_LENGTH = 50;
30 30
 
31
+/**
32
+ * The identifier of the sound to be played when new remote participant joins
33
+ * the room.
34
+ * @type {string}
35
+ */
36
+export const PARTICIPANT_JOINED_SOUND_ID = 'PARTICIPANT_JOINED_SOUND';
37
+
38
+/**
39
+ * The identifier of the sound to be played when remote participant leaves
40
+ * the room.
41
+ * @type {string}
42
+ */
43
+export const PARTICIPANT_LEFT_SOUND_ID = 'PARTICIPANT_LEFT_SOUND';
44
+
31 45
 /**
32 46
  * The set of possible XMPP MUC roles for conference participants.
33 47
  *

+ 81
- 2
react/features/base/participants/middleware.js 查看文件

@@ -2,11 +2,13 @@
2 2
 
3 3
 import UIEvents from '../../../../service/UI/UIEvents';
4 4
 
5
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
5 6
 import {
6 7
     CONFERENCE_JOINED,
7 8
     CONFERENCE_LEFT
8 9
 } from '../conference';
9 10
 import { MiddlewareRegistry } from '../redux';
11
+import { playSound, registerSound, unregisterSound } from '../sounds';
10 12
 
11 13
 import { localParticipantIdChanged } from './actions';
12 14
 import {
@@ -14,13 +16,23 @@ import {
14 16
     MUTE_REMOTE_PARTICIPANT,
15 17
     PARTICIPANT_DISPLAY_NAME_CHANGED,
16 18
     PARTICIPANT_JOINED,
19
+    PARTICIPANT_LEFT,
17 20
     PARTICIPANT_UPDATED
18 21
 } from './actionTypes';
19
-import { LOCAL_PARTICIPANT_DEFAULT_ID } from './constants';
22
+import {
23
+    LOCAL_PARTICIPANT_DEFAULT_ID,
24
+    PARTICIPANT_JOINED_SOUND_ID,
25
+    PARTICIPANT_LEFT_SOUND_ID
26
+} from './constants';
20 27
 import {
21 28
     getAvatarURLByParticipantId,
22
-    getLocalParticipant
29
+    getLocalParticipant,
30
+    getParticipantCount
23 31
 } from './functions';
32
+import {
33
+    PARTICIPANT_JOINED_SRC,
34
+    PARTICIPANT_LEFT_SRC
35
+} from './sounds';
24 36
 
25 37
 declare var APP: Object;
26 38
 
@@ -34,7 +46,18 @@ declare var APP: Object;
34 46
 MiddlewareRegistry.register(store => next => action => {
35 47
     const { conference } = store.getState()['features/base/conference'];
36 48
 
49
+    if (action.type === PARTICIPANT_JOINED
50
+        || action.type === PARTICIPANT_LEFT) {
51
+        _maybePlaySounds(store, action);
52
+    }
53
+
37 54
     switch (action.type) {
55
+    case APP_WILL_MOUNT:
56
+        _registerSounds(store);
57
+        break;
58
+    case APP_WILL_UNMOUNT:
59
+        _unregisterSounds(store);
60
+        break;
38 61
     case CONFERENCE_JOINED:
39 62
         store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
40 63
         break;
@@ -100,3 +123,59 @@ MiddlewareRegistry.register(store => next => action => {
100 123
 
101 124
     return next(action);
102 125
 });
126
+
127
+/**
128
+ * Plays sounds when participants join/leave conference.
129
+ *
130
+ * @param {Store} store - The Redux store.
131
+ * @param {Action} action - The Redux action. Should be either
132
+ * {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
133
+ * @private
134
+ * @returns {void}
135
+ */
136
+function _maybePlaySounds({ getState, dispatch }, action) {
137
+    const state = getState();
138
+    const { startAudioMuted } = state['features/base/config'];
139
+
140
+    // We're not playing sounds for local participant
141
+    // nor when the user is joining past the "startAudioMuted" limit.
142
+    // The intention there was to not play user joined notification in big
143
+    // conferences where 100th person is joining.
144
+    if (!action.participant.local
145
+        && (!startAudioMuted
146
+            || getParticipantCount(state) < startAudioMuted)) {
147
+        if (action.type === PARTICIPANT_JOINED) {
148
+            dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
149
+        } else if (action.type === PARTICIPANT_LEFT) {
150
+            dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
151
+        }
152
+    }
153
+}
154
+
155
+/**
156
+ * Registers sounds related with the participants feature.
157
+ *
158
+ * @param {Store} store - The Redux store.
159
+ * @private
160
+ * @returns {void}
161
+ */
162
+function _registerSounds({ dispatch }) {
163
+    dispatch(
164
+        registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_SRC));
165
+    dispatch(
166
+        registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_SRC));
167
+}
168
+
169
+/**
170
+ * Unregisters sounds related with the participants feature.
171
+ *
172
+ * @param {Store} store - The Redux store.
173
+ * @private
174
+ * @returns {void}
175
+ */
176
+function _unregisterSounds({ dispatch }) {
177
+    dispatch(
178
+        unregisterSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_SRC));
179
+    dispatch(
180
+        unregisterSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_SRC));
181
+}

+ 13
- 0
react/features/base/participants/sounds.native.js 查看文件

@@ -0,0 +1,13 @@
1
+/**
2
+ * Points to the sound file which will be played when new participant joins
3
+ * the conference.
4
+ */
5
+export const PARTICIPANT_JOINED_SRC
6
+    = require('../../../../sounds/joined.wav');
7
+
8
+/**
9
+ * Points to the sound file which will be played when any participant leaves
10
+ * the conference.
11
+ */
12
+export const PARTICIPANT_LEFT_SRC
13
+    = require('../../../../sounds/left.wav');

+ 11
- 0
react/features/base/participants/sounds.web.js 查看文件

@@ -0,0 +1,11 @@
1
+/**
2
+ * Points to the sound file which will be played when new participant joins
3
+ * the conference.
4
+ */
5
+export const PARTICIPANT_JOINED_SRC = 'sounds/joined.wav';
6
+
7
+/**
8
+ * Points to the sound file which will be played when any participant leaves
9
+ * the conference.
10
+ */
11
+export const PARTICIPANT_LEFT_SRC = 'sounds/left.wav';

+ 54
- 0
react/features/base/sounds/actionTypes.js 查看文件

@@ -0,0 +1,54 @@
1
+/**
2
+ * The type of a feature/internal/protected (redux) action to add an audio
3
+ * element to the sounds collection state.
4
+ *
5
+ * {
6
+ *     type: _ADD_AUDIO_ELEMENT,
7
+ *     ref: AudioElement,
8
+ *     soundId: string
9
+ * }
10
+ */
11
+export const _ADD_AUDIO_ELEMENT = Symbol('_ADD_AUDIO_ELEMENT');
12
+
13
+/**
14
+ * The type of feature/internal/protected (redux) action to remove an audio
15
+ * element for given sound identifier from the sounds collection state.
16
+ *
17
+ * {
18
+ *     type: _REMOVE_AUDIO_ELEMENT,
19
+ *     soundId: string
20
+ * }
21
+ */
22
+export const _REMOVE_AUDIO_ELEMENT = Symbol('_REMOVE_AUDIO_ELEMENT');
23
+
24
+/**
25
+ * The type of (redux) action to play a sound from the sounds collection.
26
+ *
27
+ * {
28
+ *     type: PLAY_SOUND,
29
+ *     soundId: string
30
+ * }
31
+ */
32
+export const PLAY_SOUND = Symbol('PLAY_SOUND');
33
+
34
+/**
35
+ * The type of (redux) action to register a new sound with the sounds
36
+ * collection.
37
+ *
38
+ * {
39
+ *     type: REGISTER_SOUND,
40
+ *     soundId: string
41
+ * }
42
+ */
43
+export const REGISTER_SOUND = Symbol('REGISTER_SOUND');
44
+
45
+/**
46
+ * The type of (redux) action to unregister an existing sound from the sounds
47
+ * collection.
48
+ *
49
+ * {
50
+ *     type: UNREGISTER_SOUND,
51
+ *     soundId: string
52
+ * }
53
+ */
54
+export const UNREGISTER_SOUND = Symbol('UNREGISTER_SOUND');

+ 118
- 0
react/features/base/sounds/actions.js 查看文件

@@ -0,0 +1,118 @@
1
+// @flow
2
+
3
+import type { AudioElement } from '../media';
4
+
5
+import {
6
+    _ADD_AUDIO_ELEMENT,
7
+    _REMOVE_AUDIO_ELEMENT,
8
+    PLAY_SOUND,
9
+    REGISTER_SOUND,
10
+    UNREGISTER_SOUND
11
+} from './actionTypes';
12
+
13
+/**
14
+ * Adds {@link AudioElement} instance to the base/sounds feature state for the
15
+ * {@link Sound} instance identified by the given id. After this action the
16
+ * sound can be played by dispatching the {@link PLAY_SOUND} action.
17
+ *
18
+ * @param {string} soundId - The sound identifier for which the audio element
19
+ * will be stored.
20
+ * @param {AudioElement} audioElement - The audio element which implements the
21
+ * audio playback functionality and which is backed by the sound resource
22
+ * corresponding to the {@link Sound} with the given id.
23
+ * @protected
24
+ * @returns {{
25
+ *     type: PLAY_SOUND,
26
+ *     audioElement: AudioElement,
27
+ *     soundId: string
28
+ * }}
29
+ */
30
+export function _addAudioElement(soundId: string, audioElement: AudioElement) {
31
+    return {
32
+        type: _ADD_AUDIO_ELEMENT,
33
+        audioElement,
34
+        soundId
35
+    };
36
+}
37
+
38
+/**
39
+ * The opposite of {@link _addAudioElement} which removes {@link AudioElement}
40
+ * for given sound from base/sounds state. It means that the audio resource has
41
+ * been disposed and the sound can no longer be played.
42
+ *
43
+ * @param {string} soundId - The {@link Sound} instance identifier for which the
44
+ * audio element is being removed.
45
+ * @protected
46
+ * @returns {{
47
+ *     type: _REMOVE_AUDIO_ELEMENT,
48
+ *     soundId: string
49
+ * }}
50
+ */
51
+export function _removeAudioElement(soundId: string) {
52
+    return {
53
+        type: _REMOVE_AUDIO_ELEMENT,
54
+        soundId
55
+    };
56
+}
57
+
58
+/**
59
+ * Starts playback of the sound identified by the given sound id. The action
60
+ * will have effect only if the audio resource has been loaded already.
61
+ *
62
+ * @param {string} soundId - The id of the sound to be played (the same one
63
+ * which was used in {@link registerSound} to register the sound).
64
+ * @returns {{
65
+ *     type: PLAY_SOUND,
66
+ *     soundId: string
67
+ * }}
68
+ */
69
+export function playSound(soundId: string): Object {
70
+    return {
71
+        type: PLAY_SOUND,
72
+        soundId
73
+    };
74
+}
75
+
76
+/**
77
+ * Registers a new sound for given id and a source object which can be either a
78
+ * path or a raw object depending on the platform (native vs web). It will make
79
+ * the {@link SoundCollection} render extra HTMLAudioElement which will make it
80
+ * available for playback through the {@link playSound} action.
81
+ *
82
+ * @param {string} soundId - The global identifier which identify the sound
83
+ * created for given source object.
84
+ * @param {Object|string} src - Either path to an audio file or a raw object
85
+ * which specifies the audio resource that will be associated with the given
86
+ * {@code soundId}.
87
+ * @returns {{
88
+ *     type: REGISTER_SOUND,
89
+ *     soundId: string,
90
+ *     src: (Object | string)
91
+ * }}
92
+ */
93
+export function registerSound(soundId: string, src: Object | string): Object {
94
+    return {
95
+        type: REGISTER_SOUND,
96
+        soundId,
97
+        src
98
+    };
99
+}
100
+
101
+/**
102
+ * Unregister the sound identified by the given id. It will make the
103
+ * {@link SoundCollection} component stop rendering the corresponding
104
+ * {@code HTMLAudioElement} which then should result in the audio resource
105
+ * disposal.
106
+ *
107
+ * @param {string} soundId - The identifier of the {@link Sound} to be removed.
108
+ * @returns {{
109
+ *     type: UNREGISTER_SOUND,
110
+ *     soundId: string
111
+ * }}
112
+ */
113
+export function unregisterSound(soundId: string): Object {
114
+    return {
115
+        type: UNREGISTER_SOUND,
116
+        soundId
117
+    };
118
+}

+ 160
- 0
react/features/base/sounds/components/SoundCollection.js 查看文件

@@ -0,0 +1,160 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import { Audio } from '../../media';
7
+import type { AudioElement } from '../../media';
8
+import { Fragment } from '../../react';
9
+
10
+import { _addAudioElement, _removeAudioElement } from '../actions';
11
+import type { Sound } from '../reducer';
12
+
13
+/**
14
+ * {@link SoundCollection}'s properties.
15
+ */
16
+type Props = {
17
+
18
+    /**
19
+     * Dispatches {@link _ADD_AUDIO_ELEMENT} Redux action which will store the
20
+     * {@link AudioElement} for a sound in the Redux store.
21
+     */
22
+    _addAudioElement: Function,
23
+
24
+    /**
25
+     * Dispatches {@link _REMOVE_AUDIO_ELEMENT} Redux action which will remove
26
+     * the sound's {@link AudioElement} from the Redux store.
27
+     */
28
+    _removeAudioElement: Function,
29
+
30
+    /**
31
+     * It's the 'base/sounds' reducer's state mapped to a property. It's used to
32
+     * render audio elements for every registered sound.
33
+     */
34
+    _sounds: Map<string, Sound>
35
+}
36
+
37
+/**
38
+ * Collections of all global sounds used by the app for playing audio
39
+ * notifications in response to various events. It renders <code>Audio</code>
40
+ * element for each sound registered in the base/sounds feature. When the audio
41
+ * resource is loaded it will emit add/remove audio element actions which will
42
+ * attach the element to the corresponding {@link Sound} instance in the Redux
43
+ * state. When that happens the sound can be played using the {@link playSound}
44
+ * action.
45
+ */
46
+class SoundCollection extends Component<Props> {
47
+    /**
48
+     * Implements React's {@link Component#render()}.
49
+     *
50
+     * @inheritdoc
51
+     * @returns {ReactElement}
52
+     */
53
+    render() {
54
+        let key = 0;
55
+        const sounds = [];
56
+
57
+        for (const [ soundId, sound ] of this.props._sounds.entries()) {
58
+            sounds.push(
59
+                React.createElement(
60
+                    Audio, {
61
+                        key,
62
+                        setRef: this._setRef.bind(this, soundId),
63
+                        src: sound.src
64
+                    }));
65
+            key += 1;
66
+        }
67
+
68
+        return (
69
+            <Fragment>
70
+                {
71
+                    sounds
72
+                }
73
+            </Fragment>
74
+        );
75
+    }
76
+
77
+    /**
78
+     * Set the (reference to the) {@link AudioElement} object which implements
79
+     * the audio playback functionality.
80
+     *
81
+     * @param {string} soundId - The sound Id for the audio element for which
82
+     * the callback is being executed.
83
+     * @param {AudioElement} element - The {@link AudioElement} instance
84
+     * which implements the audio playback functionality.
85
+     * @protected
86
+     * @returns {void}
87
+     */
88
+    _setRef(soundId: string, element: ?AudioElement) {
89
+        if (element) {
90
+            this.props._addAudioElement(soundId, element);
91
+        } else {
92
+            this.props._removeAudioElement(soundId);
93
+        }
94
+    }
95
+}
96
+
97
+/**
98
+ * Maps (parts of) the Redux state to {@code SoundCollection}'s props.
99
+ *
100
+ * @param {Object} state - The redux state.
101
+ * @private
102
+ * @returns {{
103
+ *     _sounds: Map<string, Sound>
104
+ * }}
105
+ */
106
+function _mapStateToProps(state) {
107
+    return {
108
+        _sounds: state['features/base/sounds']
109
+    };
110
+}
111
+
112
+/**
113
+ * Maps dispatching of some actions to React component props.
114
+ *
115
+ * @param {Function} dispatch - Redux action dispatcher.
116
+ * @private
117
+ * @returns {{
118
+ *     _addAudioElement: void,
119
+ *     _removeAudioElement: void
120
+ * }}
121
+ */
122
+export function _mapDispatchToProps(dispatch: Function) {
123
+    return {
124
+        /**
125
+         * Dispatches action to store the {@link AudioElement} for
126
+         * a {@link Sound} identified by given <tt>soundId</tt> in the Redux
127
+         * store, so that the playback can be controlled through the Redux
128
+         * actions.
129
+         *
130
+         * @param {string} soundId - A global identifier which will be used to
131
+         * identify the {@link Sound} instance for which an audio element will
132
+         * be added.
133
+         * @param {AudioElement} audioElement - The {@link AudioElement}
134
+         * instance that will be stored in the Redux state of the base/sounds
135
+         * feature, as part of the {@link Sound} object. At that point the sound
136
+         * will be ready for playback.
137
+         * @private
138
+         * @returns {void}
139
+         */
140
+        _addAudioElement(soundId: string, audioElement: AudioElement) {
141
+            dispatch(_addAudioElement(soundId, audioElement));
142
+        },
143
+
144
+        /**
145
+         * Dispatches action to remove {@link AudioElement} from the Redux
146
+         * store for specific {@link Sound}, because it is no longer part of
147
+         * the DOM tree and the audio resource will be released.
148
+         *
149
+         * @param {string} soundId - The id of the {@link Sound} instance for
150
+         * which an {@link AudioElement} will be removed from the Redux store.
151
+         * @private
152
+         * @returns {void}
153
+         */
154
+        _removeAudioElement(soundId: string) {
155
+            dispatch(_removeAudioElement(soundId));
156
+        }
157
+    };
158
+}
159
+
160
+export default connect(_mapStateToProps, _mapDispatchToProps)(SoundCollection);

+ 1
- 0
react/features/base/sounds/components/index.js 查看文件

@@ -0,0 +1 @@
1
+export { default as SoundCollection } from './SoundCollection';

+ 6
- 0
react/features/base/sounds/index.js 查看文件

@@ -0,0 +1,6 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+export * from './components';
4
+
5
+import './middleware';
6
+import './reducer';

+ 46
- 0
react/features/base/sounds/middleware.js 查看文件

@@ -0,0 +1,46 @@
1
+// @flow
2
+
3
+import { MiddlewareRegistry } from '../redux';
4
+
5
+import { PLAY_SOUND } from './actionTypes';
6
+
7
+const logger = require('jitsi-meet-logger').getLogger(__filename);
8
+
9
+/**
10
+ * Implements the entry point of the middleware of the feature base/media.
11
+ *
12
+ * @param {Store} store - The redux store.
13
+ * @returns {Function}
14
+ */
15
+MiddlewareRegistry.register(store => next => action => {
16
+    switch (action.type) {
17
+    case PLAY_SOUND:
18
+        _playSound(store, action.soundId);
19
+        break;
20
+    }
21
+
22
+    return next(action);
23
+});
24
+
25
+/**
26
+ * Plays sound from audio element registered in the Redux store.
27
+ *
28
+ * @param {Store} store - The Redux store instance.
29
+ * @param {string} soundId - Audio element identifier.
30
+ * @private
31
+ * @returns {void}
32
+ */
33
+function _playSound({ getState }, soundId) {
34
+    const sounds = getState()['features/base/sounds'];
35
+    const sound = sounds.get(soundId);
36
+
37
+    if (sound) {
38
+        if (sound.audioElement) {
39
+            sound.audioElement.play();
40
+        } else {
41
+            logger.warn(`PLAY_SOUND: sound not loaded yet for id: ${soundId}`);
42
+        }
43
+    } else {
44
+        logger.warn(`PLAY_SOUND: no sound found for id: ${soundId}`);
45
+    }
46
+}

+ 140
- 0
react/features/base/sounds/reducer.js 查看文件

@@ -0,0 +1,140 @@
1
+// @flow
2
+
3
+import type { AudioElement } from '../media';
4
+import { assign, ReducerRegistry } from '../redux';
5
+
6
+import {
7
+    _ADD_AUDIO_ELEMENT,
8
+    _REMOVE_AUDIO_ELEMENT,
9
+    REGISTER_SOUND,
10
+    UNREGISTER_SOUND
11
+} from './actionTypes';
12
+
13
+const logger = require('jitsi-meet-logger').getLogger(__filename);
14
+
15
+/**
16
+ * The structure use by this reducer to describe a sound.
17
+ */
18
+export type Sound = {
19
+
20
+    /**
21
+     * The HTMLAudioElement which implements the audio playback functionality.
22
+     * Becomes available once the sound resource gets loaded and the sound can
23
+     * not be played until that happens.
24
+     */
25
+    audioElement?: AudioElement,
26
+
27
+    /**
28
+     * This field describes the source of the audio resource to be played. It
29
+     * can be either a path to the file or an object depending on the platform
30
+     * (native vs web).
31
+     */
32
+    src: Object | string
33
+}
34
+
35
+/**
36
+ * Initial/default state of the feature {@code base/sounds}. It is a {@code Map}
37
+ * of globally stored sounds.
38
+ *
39
+ * @type {Map<string, Sound>}
40
+ */
41
+const DEFAULT_STATE = new Map();
42
+
43
+/**
44
+ * The base/sounds feature's reducer.
45
+ */
46
+ReducerRegistry.register(
47
+    'features/base/sounds',
48
+    (state = DEFAULT_STATE, action) => {
49
+        switch (action.type) {
50
+        case _ADD_AUDIO_ELEMENT:
51
+        case _REMOVE_AUDIO_ELEMENT:
52
+            return _addOrRemoveAudioElement(state, action);
53
+
54
+        case REGISTER_SOUND:
55
+            return _registerSound(state, action);
56
+
57
+        case UNREGISTER_SOUND:
58
+            return _unregisterSound(state, action);
59
+
60
+        default:
61
+            return state;
62
+        }
63
+    });
64
+
65
+/**
66
+ * Adds or removes {@link AudioElement} associated with a {@link Sound}.
67
+ *
68
+ * @param {Map<string, Sound>} state - The current Redux state of this feature.
69
+ * @param {_ADD_AUDIO_ELEMENT | _REMOVE_AUDIO_ELEMENT} action - The action to be
70
+ * handled.
71
+ * @private
72
+ * @returns {Map<string, Sound>}
73
+ */
74
+function _addOrRemoveAudioElement(state, action) {
75
+    const isAddAction = action.type === _ADD_AUDIO_ELEMENT;
76
+    const nextState = new Map(state);
77
+    const { soundId } = action;
78
+
79
+    const sound = nextState.get(soundId);
80
+
81
+    if (sound) {
82
+        if (isAddAction) {
83
+            nextState.set(soundId,
84
+                assign(sound, {
85
+                    audioElement: action.audioElement
86
+                }));
87
+        } else {
88
+            nextState.set(soundId,
89
+                assign(sound, {
90
+                    audioElement: undefined
91
+                }));
92
+        }
93
+    } else {
94
+        const actionName
95
+            = isAddAction ? '_ADD_AUDIO_ELEMENT' : '_REMOVE_AUDIO_ELEMENT';
96
+
97
+        logger.error(`${actionName}: no sound for id: ${soundId}`);
98
+    }
99
+
100
+    return nextState;
101
+}
102
+
103
+/**
104
+ * Registers a new {@link Sound} for given id and source. It will make
105
+ * the {@link SoundCollection} component render HTMLAudioElement for given
106
+ * source making it available for playback through the redux actions.
107
+ *
108
+ * @param {Map<string, Sound>} state - The current Redux state of the sounds
109
+ * features.
110
+ * @param {REGISTER_SOUND} action - The register sound action.
111
+ * @private
112
+ * @returns {Map<string, Sound>}
113
+ */
114
+function _registerSound(state, action) {
115
+    const nextState = new Map(state);
116
+
117
+    nextState.set(action.soundId, {
118
+        src: action.src
119
+    });
120
+
121
+    return nextState;
122
+}
123
+
124
+/**
125
+ * Unregisters a {@link Sound} which will make the {@link SoundCollection}
126
+ * component stop rendering the corresponding HTMLAudioElement. This will
127
+ * result further in the audio resource disposal.
128
+ *
129
+ * @param {Map<string, Sound>} state - The current Redux state of this feature.
130
+ * @param {UNREGISTER_SOUND} action - The unregister sound action.
131
+ * @private
132
+ * @returns {Map<string, Sound>}
133
+ */
134
+function _unregisterSound(state, action) {
135
+    const nextState = new Map(state);
136
+
137
+    nextState.delete(action.soundId);
138
+
139
+    return nextState;
140
+}

+ 2
- 2
react/features/base/util/uri.js 查看文件

@@ -327,12 +327,12 @@ function _standardURIToString(thiz: ?Object) {
327 327
  * the one accepted by the constructor of Web's ExternalAPI is supported on both
328 328
  * mobile/React Native and Web/React.
329 329
  *
330
- * @param {string|Object} obj - The URL to return a {@code String}
330
+ * @param {Object|string} obj - The URL to return a {@code String}
331 331
  * representation of.
332 332
  * @returns {string} - A {@code String} representation of the specified
333 333
  * {@code obj} which is supposed to represent a URL.
334 334
  */
335
-export function toURLString(obj: ?(string | Object)): ?string {
335
+export function toURLString(obj: ?(Object | string)): ?string {
336 336
     let str;
337 337
 
338 338
     switch (typeof obj) {

+ 0
- 8
react/features/filmstrip/components/Filmstrip.web.js 查看文件

@@ -158,14 +158,6 @@ class Filmstrip extends Component<*> {
158 158
                             onMouseOut = { this._onMouseOut }
159 159
                             onMouseOver = { this._onMouseOver } />
160 160
                     </div>
161
-                    <audio
162
-                        id = 'userJoined'
163
-                        preload = 'auto'
164
-                        src = 'sounds/joined.wav' />
165
-                    <audio
166
-                        id = 'userLeft'
167
-                        preload = 'auto'
168
-                        src = 'sounds/left.wav' />
169 161
                 </div>
170 162
             </div>
171 163
         );

正在加载...
取消
保存