瀏覽代碼

Outgoing call ringtones (#2949)

* fix(PresenceLabel): Use translated strings for the presence label.

* feat(sounds): Implements loop and stop functionality.

* feat(invite): Add ringtones.

* fix(invite): Code style issues.
master
hristoterezov 7 年之前
父節點
當前提交
c344a83376

+ 10
- 0
lang/main.json 查看文件

577
         "appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
577
         "appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
578
         "downloadApp": "Download the app",
578
         "downloadApp": "Download the app",
579
         "openApp": "Continue to the app"
579
         "openApp": "Continue to the app"
580
+    },
581
+    "presenceStatus": {
582
+        "invited": "Invited",
583
+        "ringing": "Ringing",
584
+        "calling": "Calling",
585
+        "connected": "Connected",
586
+        "connecting": "Connecting",
587
+        "busy": "Busy",
588
+        "rejected": "Rejected",
589
+        "ignored": "Ignored"
580
     }
590
     }
581
 }
591
 }

+ 5
- 1
modules/UI/videolayout/LargeVideoManager.js 查看文件

2
 /* eslint-disable no-unused-vars */
2
 /* eslint-disable no-unused-vars */
3
 import React from 'react';
3
 import React from 'react';
4
 import ReactDOM from 'react-dom';
4
 import ReactDOM from 'react-dom';
5
+import { I18nextProvider } from 'react-i18next';
5
 import { Provider } from 'react-redux';
6
 import { Provider } from 'react-redux';
6
 
7
 
8
+import { i18next } from '../../../react/features/base/i18n';
7
 import { PresenceLabel } from '../../../react/features/presence-status';
9
 import { PresenceLabel } from '../../../react/features/presence-status';
8
 /* eslint-enable no-unused-vars */
10
 /* eslint-enable no-unused-vars */
9
 
11
 
456
         if (presenceLabelContainer.length) {
458
         if (presenceLabelContainer.length) {
457
             ReactDOM.render(
459
             ReactDOM.render(
458
                 <Provider store = { APP.store }>
460
                 <Provider store = { APP.store }>
459
-                    <PresenceLabel participantID = { id } />
461
+                    <I18nextProvider i18n = { i18next }>
462
+                        <PresenceLabel participantID = { id } />
463
+                    </I18nextProvider>
460
                 </Provider>,
464
                 </Provider>,
461
                 presenceLabelContainer.get(0));
465
                 presenceLabelContainer.get(0));
462
         }
466
         }

+ 3
- 1
modules/UI/videolayout/RemoteVideo.js 查看文件

609
     if (presenceLabelContainer) {
609
     if (presenceLabelContainer) {
610
         ReactDOM.render(
610
         ReactDOM.render(
611
             <Provider store = { APP.store }>
611
             <Provider store = { APP.store }>
612
-                <PresenceLabel participantID = { this.id } />
612
+                <I18nextProvider i18n = { i18next }>
613
+                    <PresenceLabel participantID = { this.id } />
614
+                </I18nextProvider>
613
             </Provider>,
615
             </Provider>,
614
             presenceLabelContainer);
616
             presenceLabelContainer);
615
     }
617
     }

+ 3
- 1
react/features/base/media/components/AbstractAudio.js 查看文件

7
  * playback.
7
  * playback.
8
  */
8
  */
9
 export type AudioElement = {
9
 export type AudioElement = {
10
+    currentTime?: number,
10
     pause: () => void,
11
     pause: () => void,
11
     play: () => void,
12
     play: () => void,
12
     setSinkId?: string => void
13
     setSinkId?: string => void
32
      * @type {Object | string}
33
      * @type {Object | string}
33
      */
34
      */
34
     src: Object | string,
35
     src: Object | string,
35
-    stream: Object
36
+    stream: Object,
37
+    loop?: ?boolean
36
 }
38
 }
37
 
39
 
38
 /**
40
 /**

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

74
         // writing to not render anything.
74
         // writing to not render anything.
75
         return null;
75
         return null;
76
     }
76
     }
77
+
78
+    /**
79
+     * Stops the sound if it's currently playing.
80
+     *
81
+     * @returns {void}
82
+     */
83
+    stop() {
84
+        // Currently not implemented for mobile. If needed, a possible
85
+        // implementation is:
86
+        // if (this._sound) {
87
+        //     this._sound.stop();
88
+        // }
89
+    }
77
 }
90
 }

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

41
      * @returns {ReactElement}
41
      * @returns {ReactElement}
42
      */
42
      */
43
     render() {
43
     render() {
44
+        const loop = this.props.loop ? 'true' : null;
45
+
44
         return (
46
         return (
45
             <audio
47
             <audio
48
+                loop = { loop }
46
                 onCanPlayThrough = { this._onCanPlayThrough }
49
                 onCanPlayThrough = { this._onCanPlayThrough }
47
                 preload = 'auto'
50
                 preload = 'auto'
48
 
51
 
52
         );
55
         );
53
     }
56
     }
54
 
57
 
58
+    /**
59
+     * Stops the audio HTML element.
60
+     *
61
+     * @returns {void}
62
+     */
63
+    stop() {
64
+        if (this._ref) {
65
+            this._ref.pause();
66
+
67
+            // $FlowFixMe
68
+            this._ref.currentTime = 0;
69
+        }
70
+    }
71
+
55
     /**
72
     /**
56
      * If audio element reference has been set and the file has been
73
      * If audio element reference has been set and the file has been
57
      * loaded then {@link setAudioElementImpl} will be called to eventually add
74
      * loaded then {@link setAudioElementImpl} will be called to eventually add

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

42
  */
42
  */
43
 export const REGISTER_SOUND = Symbol('REGISTER_SOUND');
43
 export const REGISTER_SOUND = Symbol('REGISTER_SOUND');
44
 
44
 
45
+/**
46
+ * The type of (redux) action to stop a sound from the sounds collection.
47
+ *
48
+ * {
49
+ *     type: STOP_SOUND,
50
+ *     soundId: string
51
+ * }
52
+ */
53
+export const STOP_SOUND = Symbol('STOP_SOUND');
54
+
45
 /**
55
 /**
46
  * The type of (redux) action to unregister an existing sound from the sounds
56
  * The type of (redux) action to unregister an existing sound from the sounds
47
  * collection.
57
  * collection.

+ 28
- 3
react/features/base/sounds/actions.js 查看文件

7
     _REMOVE_AUDIO_ELEMENT,
7
     _REMOVE_AUDIO_ELEMENT,
8
     PLAY_SOUND,
8
     PLAY_SOUND,
9
     REGISTER_SOUND,
9
     REGISTER_SOUND,
10
+    STOP_SOUND,
10
     UNREGISTER_SOUND
11
     UNREGISTER_SOUND
11
 } from './actionTypes';
12
 } from './actionTypes';
12
 import { getSoundsPath } from './functions';
13
 import { getSoundsPath } from './functions';
84
  * created for given source object.
85
  * created for given source object.
85
  * @param {string} soundName - The name of bundled audio file that will be
86
  * @param {string} soundName - The name of bundled audio file that will be
86
  * associated with the given {@code soundId}.
87
  * associated with the given {@code soundId}.
88
+ * @param {Object} options - Optional paramaters.
89
+ * @param {boolean} options.loop - True in order to loop the sound.
87
  * @returns {{
90
  * @returns {{
88
  *     type: REGISTER_SOUND,
91
  *     type: REGISTER_SOUND,
89
  *     soundId: string,
92
  *     soundId: string,
90
- *     src: string
93
+ *     src: string,
94
+ *     options: {
95
+ *          loop: boolean
96
+ *     }
91
  * }}
97
  * }}
92
  */
98
  */
93
-export function registerSound(soundId: string, soundName: string): Object {
99
+export function registerSound(
100
+        soundId: string, soundName: string, options: Object = {}): Object {
94
     return {
101
     return {
95
         type: REGISTER_SOUND,
102
         type: REGISTER_SOUND,
96
         soundId,
103
         soundId,
97
-        src: `${getSoundsPath()}/${soundName}`
104
+        src: `${getSoundsPath()}/${soundName}`,
105
+        options
106
+    };
107
+}
108
+
109
+/**
110
+ * Stops playback of the sound identified by the given sound id.
111
+ *
112
+ * @param {string} soundId - The id of the sound to be stopped (the same one
113
+ * which was used in {@link registerSound} to register the sound).
114
+ * @returns {{
115
+ *     type: STOP_SOUND,
116
+ *     soundId: string
117
+ * }}
118
+ */
119
+export function stopSound(soundId: string): Object {
120
+    return {
121
+        type: STOP_SOUND,
122
+        soundId
98
     };
123
     };
99
 }
124
 }
100
 
125
 

+ 4
- 1
react/features/base/sounds/components/SoundCollection.js 查看文件

55
         const sounds = [];
55
         const sounds = [];
56
 
56
 
57
         for (const [ soundId, sound ] of this.props._sounds.entries()) {
57
         for (const [ soundId, sound ] of this.props._sounds.entries()) {
58
+            const { options, src } = sound;
59
+
58
             sounds.push(
60
             sounds.push(
59
                 React.createElement(
61
                 React.createElement(
60
                     Audio, {
62
                     Audio, {
61
                         key,
63
                         key,
62
                         setRef: this._setRef.bind(this, soundId),
64
                         setRef: this._setRef.bind(this, soundId),
63
-                        src: sound.src
65
+                        src,
66
+                        loop: options.loop
64
                     }));
67
                     }));
65
             key += 1;
68
             key += 1;
66
         }
69
         }

+ 29
- 1
react/features/base/sounds/middleware.js 查看文件

2
 
2
 
3
 import { MiddlewareRegistry } from '../redux';
3
 import { MiddlewareRegistry } from '../redux';
4
 
4
 
5
-import { PLAY_SOUND } from './actionTypes';
5
+import { PLAY_SOUND, STOP_SOUND } from './actionTypes';
6
 
6
 
7
 const logger = require('jitsi-meet-logger').getLogger(__filename);
7
 const logger = require('jitsi-meet-logger').getLogger(__filename);
8
 
8
 
17
     case PLAY_SOUND:
17
     case PLAY_SOUND:
18
         _playSound(store, action.soundId);
18
         _playSound(store, action.soundId);
19
         break;
19
         break;
20
+    case STOP_SOUND:
21
+        _stopSound(store, action.soundId);
22
+        break;
20
     }
23
     }
21
 
24
 
22
     return next(action);
25
     return next(action);
44
         logger.warn(`PLAY_SOUND: no sound found for id: ${soundId}`);
47
         logger.warn(`PLAY_SOUND: no sound found for id: ${soundId}`);
45
     }
48
     }
46
 }
49
 }
50
+
51
+/**
52
+ * Stop sound from audio element registered in the Redux store.
53
+ *
54
+ * @param {Store} store - The Redux store instance.
55
+ * @param {string} soundId - Audio element identifier.
56
+ * @private
57
+ * @returns {void}
58
+ */
59
+function _stopSound({ getState }, soundId) {
60
+    const sounds = getState()['features/base/sounds'];
61
+    const sound = sounds.get(soundId);
62
+
63
+    if (sound) {
64
+        const { audioElement } = sound;
65
+
66
+        if (audioElement) {
67
+            audioElement.stop();
68
+        } else {
69
+            logger.warn(`STOP_SOUND: sound not loaded yet for id: ${soundId}`);
70
+        }
71
+    } else {
72
+        logger.warn(`STOP_SOUND: no sound found for id: ${soundId}`);
73
+    }
74
+}

+ 8
- 2
react/features/base/sounds/reducer.js 查看文件

29
      * can be either a path to the file or an object depending on the platform
29
      * can be either a path to the file or an object depending on the platform
30
      * (native vs web).
30
      * (native vs web).
31
      */
31
      */
32
-    src: Object | string
32
+    src: Object | string,
33
+
34
+    /**
35
+     * This field is container for all optional parameters related to the sound.
36
+     */
37
+    options: Object
33
 }
38
 }
34
 
39
 
35
 /**
40
 /**
115
     const nextState = new Map(state);
120
     const nextState = new Map(state);
116
 
121
 
117
     nextState.set(action.soundId, {
122
     nextState.set(action.soundId, {
118
-        src: action.src
123
+        src: action.src,
124
+        options: action.options
119
     });
125
     });
120
 
126
 
121
     return nextState;
127
     return nextState;

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

1
+/**
2
+ * The identifier of the sound to be played when the status of an outgoing call
3
+ * is ringing.
4
+ *
5
+ * @type {string}
6
+ */
7
+export const OUTGOING_CALL_RINGING_SOUND_ID = 'OUTGOING_CALL_RINGING_SOUND_ID';
8
+
9
+/**
10
+ * The identifier of the sound to be played when outgoing call is started.
11
+ *
12
+ * @type {string}
13
+ */
14
+export const OUTGOING_CALL_START_SOUND_ID = 'OUTGOING_CALL_START_SOUND_ID';

+ 104
- 1
react/features/invite/middleware.any.js 查看文件

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
4
+import {
5
+    getParticipantById,
6
+    PARTICIPANT_UPDATED,
7
+    PARTICIPANT_LEFT
8
+} from '../base/participants';
3
 import { MiddlewareRegistry } from '../base/redux';
9
 import { MiddlewareRegistry } from '../base/redux';
10
+import {
11
+    playSound,
12
+    registerSound,
13
+    stopSound,
14
+    unregisterSound
15
+} from '../base/sounds';
16
+import {
17
+    CALLING,
18
+    INVITED,
19
+    RINGING
20
+} from '../presence-status';
4
 
21
 
5
 import { UPDATE_DIAL_IN_NUMBERS_FAILED } from './actionTypes';
22
 import { UPDATE_DIAL_IN_NUMBERS_FAILED } from './actionTypes';
23
+import {
24
+    OUTGOING_CALL_START_SOUND_ID,
25
+    OUTGOING_CALL_RINGING_SOUND_ID
26
+} from './constants';
27
+import {
28
+    OUTGOING_CALL_START_FILE,
29
+    OUTGOING_CALL_RINGING_FILE
30
+} from './sounds';
6
 
31
 
7
 const logger = require('jitsi-meet-logger').getLogger(__filename);
32
 const logger = require('jitsi-meet-logger').getLogger(__filename);
8
 
33
 
34
+declare var interfaceConfig: Object;
35
+
9
 /**
36
 /**
10
  * The middleware of the feature invite common to mobile/react-native and
37
  * The middleware of the feature invite common to mobile/react-native and
11
  * Web/React.
38
  * Web/React.
13
  * @param {Store} store - The redux store.
40
  * @param {Store} store - The redux store.
14
  * @returns {Function}
41
  * @returns {Function}
15
  */
42
  */
16
-// eslint-disable-next-line no-unused-vars
17
 MiddlewareRegistry.register(store => next => action => {
43
 MiddlewareRegistry.register(store => next => action => {
44
+    let oldParticipantPresence;
45
+
46
+    if (action.type === PARTICIPANT_UPDATED
47
+        || action.type === PARTICIPANT_LEFT) {
48
+        oldParticipantPresence
49
+            = _getParticipantPresence(store.getState(), action.participant.id);
50
+    }
51
+
18
     const result = next(action);
52
     const result = next(action);
19
 
53
 
20
     switch (action.type) {
54
     switch (action.type) {
55
+    case APP_WILL_MOUNT:
56
+        store.dispatch(
57
+            registerSound(
58
+                OUTGOING_CALL_START_SOUND_ID,
59
+                OUTGOING_CALL_START_FILE));
60
+
61
+        store.dispatch(
62
+            registerSound(
63
+                OUTGOING_CALL_RINGING_SOUND_ID,
64
+                OUTGOING_CALL_RINGING_FILE,
65
+                { loop: true }));
66
+        break;
67
+
68
+    case APP_WILL_UNMOUNT:
69
+        store.dispatch(unregisterSound(OUTGOING_CALL_START_SOUND_ID));
70
+        store.dispatch(unregisterSound(OUTGOING_CALL_RINGING_SOUND_ID));
71
+        break;
72
+
73
+    case PARTICIPANT_LEFT:
74
+    case PARTICIPANT_UPDATED: {
75
+        const newParticipantPresence
76
+            = _getParticipantPresence(store.getState(), action.participant.id);
77
+
78
+        if (oldParticipantPresence === newParticipantPresence) {
79
+            break;
80
+        }
81
+
82
+        switch (oldParticipantPresence) {
83
+        case CALLING:
84
+        case INVITED:
85
+            store.dispatch(stopSound(OUTGOING_CALL_START_SOUND_ID));
86
+            break;
87
+        case RINGING:
88
+            store.dispatch(stopSound(OUTGOING_CALL_RINGING_SOUND_ID));
89
+            break;
90
+        }
91
+
92
+        switch (newParticipantPresence) {
93
+        case CALLING:
94
+        case INVITED:
95
+            store.dispatch(playSound(OUTGOING_CALL_START_SOUND_ID));
96
+            break;
97
+        case RINGING:
98
+            store.dispatch(playSound(OUTGOING_CALL_RINGING_SOUND_ID));
99
+        }
100
+
101
+        break;
102
+    }
21
     case UPDATE_DIAL_IN_NUMBERS_FAILED:
103
     case UPDATE_DIAL_IN_NUMBERS_FAILED:
22
         logger.error(
104
         logger.error(
23
             'Error encountered while fetching dial-in numbers:',
105
             'Error encountered while fetching dial-in numbers:',
27
 
109
 
28
     return result;
110
     return result;
29
 });
111
 });
112
+
113
+/**
114
+ * Returns the presence status of a participant associated with the passed id.
115
+ *
116
+ * @param {Object} state - The redux state.
117
+ * @param {string} id - The id of the participant.
118
+ * @returns {string} - The presence status.
119
+ */
120
+function _getParticipantPresence(state, id) {
121
+    if (!id) {
122
+        return undefined;
123
+    }
124
+    const participants = state['features/base/participants'];
125
+    const participantById = getParticipantById(participants, id);
126
+
127
+    if (!participantById) {
128
+        return undefined;
129
+    }
130
+
131
+    return participantById.presence;
132
+}

+ 11
- 0
react/features/invite/sounds.js 查看文件

1
+/**
2
+ * The name of the sound file which will be played when the status of an
3
+ * outgoing call is ringing.
4
+ */
5
+export const OUTGOING_CALL_RINGING_FILE = 'outgoingRinging.wav';
6
+
7
+/**
8
+ * The name of the sound file which will be played when outgoing call is
9
+ * started.
10
+ */
11
+export const OUTGOING_CALL_START_FILE = 'outgoingStart.wav';

+ 32
- 3
react/features/presence-status/components/PresenceLabel.js 查看文件

2
 import React, { Component } from 'react';
2
 import React, { Component } from 'react';
3
 import { connect } from 'react-redux';
3
 import { connect } from 'react-redux';
4
 
4
 
5
+import { translate } from '../../base/i18n';
5
 import { getParticipantById } from '../../base/participants';
6
 import { getParticipantById } from '../../base/participants';
6
 
7
 
8
+import { STATUS_TO_I18N_KEY } from '../constants';
9
+
7
 /**
10
 /**
8
  * React {@code Component} for displaying the current presence status of a
11
  * React {@code Component} for displaying the current presence status of a
9
  * participant.
12
  * participant.
35
         /**
38
         /**
36
          * The ID of the participant whose presence status shoul display.
39
          * The ID of the participant whose presence status shoul display.
37
          */
40
          */
38
-        participantID: PropTypes.string
41
+        participantID: PropTypes.string,
42
+
43
+        /**
44
+         * Invoked to obtain translated strings.
45
+         */
46
+        t: PropTypes.func
39
     };
47
     };
40
 
48
 
41
     /**
49
     /**
51
             <div
59
             <div
52
                 className
60
                 className
53
                     = { `presence-label ${_presence ? '' : 'no-presence'}` }>
61
                     = { `presence-label ${_presence ? '' : 'no-presence'}` }>
54
-                { _presence }
62
+                { this._getPresenceText() }
55
             </div>
63
             </div>
56
         );
64
         );
57
     }
65
     }
66
+
67
+    /**
68
+     * Returns the text associated with the current presence status.
69
+     *
70
+     * @returns {string}
71
+     */
72
+    _getPresenceText() {
73
+        const { _presence, t } = this.props;
74
+
75
+        if (!_presence) {
76
+            return null;
77
+        }
78
+
79
+        const i18nKey = STATUS_TO_I18N_KEY[_presence];
80
+
81
+        if (!i18nKey) { // fallback to status value
82
+            return _presence;
83
+        }
84
+
85
+        return t(i18nKey);
86
+    }
58
 }
87
 }
59
 
88
 
60
 /**
89
 /**
79
     };
108
     };
80
 }
109
 }
81
 
110
 
82
-export default connect(_mapStateToProps)(PresenceLabel);
111
+export default translate(connect(_mapStateToProps)(PresenceLabel));

+ 70
- 0
react/features/presence-status/constants.js 查看文件

1
+/**
2
+ * Тhe status for a participant when it's invited to a conference.
3
+ *
4
+ * @type {string}
5
+ */
6
+export const INVITED = 'Invited';
7
+
8
+/**
9
+ * Тhe status for a participant when a call has been initiated.
10
+ *
11
+ * @type {string}
12
+ */
13
+export const CALLING = 'Calling';
14
+
15
+/**
16
+ * Тhe status for a participant when the invite is received and its device(s)
17
+ * are ringing.
18
+ *
19
+ * @type {string}
20
+ */
21
+export const RINGING = 'Ringing';
22
+
23
+/**
24
+ * A status for a participant that indicates the call is connected.
25
+ * NOTE: Currently used for phone numbers only.
26
+ *
27
+ * @type {string}
28
+ */
29
+export const CONNECTED = 'Connected';
30
+
31
+/**
32
+ * A status for a participant that indicates the call is in process of
33
+ * connecting.
34
+ * NOTE: Currently used for phone numbers only.
35
+ *
36
+ * @type {string}
37
+ */
38
+export const CONNECTING = 'Connecting';
39
+
40
+/**
41
+ * The status for a participant when the invitation is received but the user
42
+ * has responded with busy message.
43
+ */
44
+export const BUSY = 'Busy';
45
+
46
+/**
47
+ * The status for a participant when the invitation is rejected.
48
+ */
49
+export const REJECTED = 'Rejected';
50
+
51
+/**
52
+ * The status for a participant when the invitation is ignored.
53
+ */
54
+export const IGNORED = 'Ignored';
55
+
56
+/**
57
+ * Maps the presence status values to i18n translation keys.
58
+ *
59
+ * @type {Object<String, String>}
60
+ */
61
+export const STATUS_TO_I18N_KEY = {
62
+    'Invited': 'presenceStatus.invited',
63
+    'Ringing': 'presenceStatus.ringing',
64
+    'Calling': 'presenceStatus.calling',
65
+    'Connected': 'presenceStatus.connected',
66
+    'Connecting': 'presenceStatus.connecting',
67
+    'Busy': 'presenceStatus.busy',
68
+    'Rejected': 'presenceStatus.rejected',
69
+    'Ignored': 'presenceStatus.ignored'
70
+};

+ 1
- 0
react/features/presence-status/index.js 查看文件

1
 export * from './components';
1
 export * from './components';
2
+export * from './constants';

二進制
sounds/outgoingRinging.wav 查看文件


二進制
sounds/outgoingStart.wav 查看文件


Loading…
取消
儲存