Quellcode durchsuchen

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.
j8
hristoterezov vor 7 Jahren
Ursprung
Commit
c344a83376

+ 10
- 0
lang/main.json Datei anzeigen

@@ -577,5 +577,15 @@
577 577
         "appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
578 578
         "downloadApp": "Download the app",
579 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 Datei anzeigen

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

+ 3
- 1
modules/UI/videolayout/RemoteVideo.js Datei anzeigen

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

+ 3
- 1
react/features/base/media/components/AbstractAudio.js Datei anzeigen

@@ -7,6 +7,7 @@ import { Component } from 'react';
7 7
  * playback.
8 8
  */
9 9
 export type AudioElement = {
10
+    currentTime?: number,
10 11
     pause: () => void,
11 12
     play: () => void,
12 13
     setSinkId?: string => void
@@ -32,7 +33,8 @@ type Props = {
32 33
      * @type {Object | string}
33 34
      */
34 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 Datei anzeigen

@@ -74,4 +74,17 @@ export default class Audio extends AbstractAudio {
74 74
         // writing to not render anything.
75 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 Datei anzeigen

@@ -41,8 +41,11 @@ export default class Audio extends AbstractAudio {
41 41
      * @returns {ReactElement}
42 42
      */
43 43
     render() {
44
+        const loop = this.props.loop ? 'true' : null;
45
+
44 46
         return (
45 47
             <audio
48
+                loop = { loop }
46 49
                 onCanPlayThrough = { this._onCanPlayThrough }
47 50
                 preload = 'auto'
48 51
 
@@ -52,6 +55,20 @@ export default class Audio extends AbstractAudio {
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 73
      * If audio element reference has been set and the file has been
57 74
      * loaded then {@link setAudioElementImpl} will be called to eventually add

+ 10
- 0
react/features/base/sounds/actionTypes.js Datei anzeigen

@@ -42,6 +42,16 @@ export const PLAY_SOUND = Symbol('PLAY_SOUND');
42 42
  */
43 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 56
  * The type of (redux) action to unregister an existing sound from the sounds
47 57
  * collection.

+ 28
- 3
react/features/base/sounds/actions.js Datei anzeigen

@@ -7,6 +7,7 @@ import {
7 7
     _REMOVE_AUDIO_ELEMENT,
8 8
     PLAY_SOUND,
9 9
     REGISTER_SOUND,
10
+    STOP_SOUND,
10 11
     UNREGISTER_SOUND
11 12
 } from './actionTypes';
12 13
 import { getSoundsPath } from './functions';
@@ -84,17 +85,41 @@ export function playSound(soundId: string): Object {
84 85
  * created for given source object.
85 86
  * @param {string} soundName - The name of bundled audio file that will be
86 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 90
  * @returns {{
88 91
  *     type: REGISTER_SOUND,
89 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 101
     return {
95 102
         type: REGISTER_SOUND,
96 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 Datei anzeigen

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

+ 29
- 1
react/features/base/sounds/middleware.js Datei anzeigen

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { MiddlewareRegistry } from '../redux';
4 4
 
5
-import { PLAY_SOUND } from './actionTypes';
5
+import { PLAY_SOUND, STOP_SOUND } from './actionTypes';
6 6
 
7 7
 const logger = require('jitsi-meet-logger').getLogger(__filename);
8 8
 
@@ -17,6 +17,9 @@ MiddlewareRegistry.register(store => next => action => {
17 17
     case PLAY_SOUND:
18 18
         _playSound(store, action.soundId);
19 19
         break;
20
+    case STOP_SOUND:
21
+        _stopSound(store, action.soundId);
22
+        break;
20 23
     }
21 24
 
22 25
     return next(action);
@@ -44,3 +47,28 @@ function _playSound({ getState }, soundId) {
44 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 Datei anzeigen

@@ -29,7 +29,12 @@ export type Sound = {
29 29
      * can be either a path to the file or an object depending on the platform
30 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,7 +120,8 @@ function _registerSound(state, action) {
115 120
     const nextState = new Map(state);
116 121
 
117 122
     nextState.set(action.soundId, {
118
-        src: action.src
123
+        src: action.src,
124
+        options: action.options
119 125
     });
120 126
 
121 127
     return nextState;

+ 14
- 0
react/features/invite/constants.js Datei anzeigen

@@ -0,0 +1,14 @@
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 Datei anzeigen

@@ -1,11 +1,38 @@
1 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 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 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 32
 const logger = require('jitsi-meet-logger').getLogger(__filename);
8 33
 
34
+declare var interfaceConfig: Object;
35
+
9 36
 /**
10 37
  * The middleware of the feature invite common to mobile/react-native and
11 38
  * Web/React.
@@ -13,11 +40,66 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
13 40
  * @param {Store} store - The redux store.
14 41
  * @returns {Function}
15 42
  */
16
-// eslint-disable-next-line no-unused-vars
17 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 52
     const result = next(action);
19 53
 
20 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 103
     case UPDATE_DIAL_IN_NUMBERS_FAILED:
22 104
         logger.error(
23 105
             'Error encountered while fetching dial-in numbers:',
@@ -27,3 +109,24 @@ MiddlewareRegistry.register(store => next => action => {
27 109
 
28 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 Datei anzeigen

@@ -0,0 +1,11 @@
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 Datei anzeigen

@@ -2,8 +2,11 @@ import PropTypes from 'prop-types';
2 2
 import React, { Component } from 'react';
3 3
 import { connect } from 'react-redux';
4 4
 
5
+import { translate } from '../../base/i18n';
5 6
 import { getParticipantById } from '../../base/participants';
6 7
 
8
+import { STATUS_TO_I18N_KEY } from '../constants';
9
+
7 10
 /**
8 11
  * React {@code Component} for displaying the current presence status of a
9 12
  * participant.
@@ -35,7 +38,12 @@ class PresenceLabel extends Component {
35 38
         /**
36 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,10 +59,31 @@ class PresenceLabel extends Component {
51 59
             <div
52 60
                 className
53 61
                     = { `presence-label ${_presence ? '' : 'no-presence'}` }>
54
-                { _presence }
62
+                { this._getPresenceText() }
55 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,4 +108,4 @@ function _mapStateToProps(state, ownProps) {
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 Datei anzeigen

@@ -0,0 +1,70 @@
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 Datei anzeigen

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

BIN
sounds/outgoingRinging.wav Datei anzeigen


BIN
sounds/outgoingStart.wav Datei anzeigen


Laden…
Abbrechen
Speichern