Browse Source

initial commit

tags/v0.0.2
Andrei Gavrilescu 5 years ago
parent
commit
21b531900a

+ 19
- 8
JitsiConference.js View File

@@ -13,10 +13,11 @@ import JitsiTrackError from './JitsiTrackError';
13 13
 import * as JitsiTrackErrors from './JitsiTrackErrors';
14 14
 import * as JitsiTrackEvents from './JitsiTrackEvents';
15 15
 import authenticateAndUpgradeRole from './authenticateAndUpgradeRole';
16
-import P2PDominantSpeakerDetection from './modules/P2PDominantSpeakerDetection';
16
+import P2PDominantSpeakerDetection from './modules/detection/P2PDominantSpeakerDetection';
17 17
 import RTC from './modules/RTC/RTC';
18
-import TalkMutedDetection from './modules/TalkMutedDetection';
19
-import NoAudioSignalDetection from './modules/NoAudioSignalDetection';
18
+import VADTalkMutedDetection from './modules/detection/VADTalkMutedDetection';
19
+import * as DetectionEvents from './modules/detection/DetectionEvents';
20
+import NoAudioSignalDetection from './modules/detection/NoAudioSignalDetection';
20 21
 import browser from './modules/browser';
21 22
 import ConnectionQuality from './modules/connectivity/ConnectionQuality';
22 23
 import IceFailedNotification
@@ -275,6 +276,9 @@ JitsiConference.resourceCreator = function(jid, isAuthenticatedUser) {
275 276
  * @param options.connection {JitsiConnection} overrides this.connection
276 277
  */
277 278
 JitsiConference.prototype._init = function(options = {}) {
279
+
280
+    logger.info('[ADBG] Init concerence v.0.1!');
281
+
278 282
     // Override connection and xmpp properties (Useful if the connection
279 283
     // reloaded)
280 284
     if (options.connection) {
@@ -373,15 +377,22 @@ JitsiConference.prototype._init = function(options = {}) {
373 377
     this.eventManager.setupStatisticsListeners();
374 378
 
375 379
     if (config.enableTalkWhileMuted) {
376
-        // eslint-disable-next-line no-new
377
-        new TalkMutedDetection(
378
-            this,
379
-            () =>
380
+
381
+        if (config.vadProcessor) {
382
+            // eslint-disable-next-line no-new
383
+            this._talkWhileMutedDetection = new VADTalkMutedDetection(this, config.vadProcessor);
384
+            this._talkWhileMutedDetection.on(DetectionEvents.VAD_TALK_WHILE_MUTED, () =>
380 385
                 this.eventEmitter.emit(JitsiConferenceEvents.TALK_WHILE_MUTED));
386
+
387
+        } else {
388
+            logger.info('[ADBG] No vadProcessor set!');
389
+        }
390
+
381 391
     }
382 392
 
383 393
     // Generates events based on no audio input detector.
384
-    if (config.enableNoAudioDetection) {
394
+    // eslint-disable-next-line
395
+    if (true) {
385 396
         // eslint-disable-next-line no-new
386 397
         new NoAudioSignalDetection(this, () =>
387 398
             this.eventEmitter.emit(JitsiConferenceEvents.NO_AUDIO_INPUT));

+ 9
- 0
modules/detection/DetectionEvents.js View File

@@ -0,0 +1,9 @@
1
+/**
2
+ * A media track mute status was changed.
3
+ */
4
+export const VAD_SCORE_PUBLISHED = 'detection.vad_score_published';
5
+
6
+/**
7
+ * A media track mute status was changed.
8
+ */
9
+export const VAD_TALK_WHILE_MUTED = 'detection.vad_talk_while_muted';

modules/NoAudioSignalDetection.js → modules/detection/NoAudioSignalDetection.js View File

@@ -1,4 +1,4 @@
1
-import * as JitsiConferenceEvents from '../JitsiConferenceEvents';
1
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
2 2
 
3 3
 // We wait a certain time interval for constant silence input from the current device to account for
4 4
 // potential abnormalities and for a better use experience i.e. don't generate event the instant
@@ -23,7 +23,6 @@ export default class NoAudioSignalDetection {
23 23
 
24 24
         conference.statistics.addAudioLevelListener(this._audioLevel.bind(this));
25 25
         conference.on(JitsiConferenceEvents.TRACK_ADDED, this._trackAdded.bind(this));
26
-
27 26
     }
28 27
 
29 28
     /**
@@ -32,7 +31,6 @@ export default class NoAudioSignalDetection {
32 31
      * @returns {boolean}
33 32
      */
34 33
     _hasSilencePeriodElapsed() {
35
-
36 34
         const currentDate = new Date();
37 35
         const elapsedSec = (currentDate.getTime() - this._firstSilentSignalDate.getTime()) / 1000;
38 36
 
@@ -52,7 +50,7 @@ export default class NoAudioSignalDetection {
52 50
         if (!this._firstSilentSignalDate) {
53 51
             this._firstSilentSignalDate = new Date();
54 52
 
55
-        // If the configured interval has elapsed trigger the callback
53
+            // If the configured interval has elapsed trigger the callback
56 54
         } else if (this._hasSilencePeriodElapsed()) {
57 55
             this._eventFired = true;
58 56
             this._callback();
@@ -69,7 +67,6 @@ export default class NoAudioSignalDetection {
69 67
      * @param {boolean} isLocal - true for local/send streams or false for remote/receive streams.
70 68
      */
71 69
     _audioLevel(tpc, ssrc, audioLevel, isLocal) {
72
-
73 70
         // We are interested in the local audio stream if the event was not triggered on this device.
74 71
         if (!isLocal || !this._audioTrack || this._eventFired) {
75 72
             return;

modules/P2PDominantSpeakerDetection.js → modules/detection/P2PDominantSpeakerDetection.js View File

@@ -1,5 +1,5 @@
1
-import * as JitsiConferenceEvents from '../JitsiConferenceEvents';
2
-import RTCEvents from '../service/RTC/RTCEvents';
1
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
2
+import RTCEvents from '../../service/RTC/RTCEvents';
3 3
 
4 4
 /**
5 5
  * The value which we use to say, every sound over this threshold

modules/TalkMutedDetection.js → modules/detection/TalkMutedDetection.js View File

@@ -1,4 +1,4 @@
1
-import * as JitsiConferenceEvents from '../JitsiConferenceEvents';
1
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
2 2
 
3 3
 /**
4 4
  * The value which we use to say, every sound over this threshold

+ 155
- 0
modules/detection/TrackVADEmitter.js View File

@@ -0,0 +1,155 @@
1
+import EventEmitter from 'events';
2
+import RTC from '../RTC/RTC';
3
+import { VAD_SCORE_PUBLISHED } from './DetectionEvents';
4
+
5
+/**
6
+ * Connects an audio JitsiLocalTrack to a vadProcessor using WebAudio ScriptProcessorNode.
7
+ * Once an object is created audio from the local track flows through the ScriptProcessorNode as raw PCM.
8
+ * The PCM is processed by the injected vad module and a voice activity detection score is obtained, the
9
+ * score is published to consumers via an EventEmitter.
10
+ * After work is done with this service the destroy method needs to be called for a proper cleanup.
11
+ */
12
+export default class TrackVADEmitter extends EventEmitter {
13
+    /**
14
+     * Constructor.
15
+     *
16
+     * @param {number} procNodeSampleRate - Sample rate of the ScriptProcessorNode. Possible values  256, 512, 1024,
17
+     *  2048, 4096, 8192, 16384. Passing other values will default to closes neighbor.
18
+     * @param {Object} vadProcessor - adapter that allows us to calculate VAD score
19
+     * for PCM samples.
20
+     * @param {Object} jitsiLocalTrack - JitsiLocalTrack corresponding to micDeviceId.
21
+     */
22
+    constructor(procNodeSampleRate, vadProcessor, jitsiLocalTrack) {
23
+        super();
24
+        this._procNodeSampleRate = procNodeSampleRate;
25
+        this._vadProcessor = vadProcessor;
26
+        this._localTrack = jitsiLocalTrack;
27
+        this._micDeviceId = jitsiLocalTrack.getDeviceId();
28
+        this._bufferResidue = new Float32Array([]);
29
+        this._audioContext = new AudioContext({ sampleRate: 44100 });
30
+
31
+        this._vadSampleSize = vadProcessor.getSampleLength();
32
+        this._onAudioProcess = this._onAudioProcess.bind(this);
33
+
34
+        this._initializeAudioContext();
35
+        this._connectAudioGraph();
36
+    }
37
+
38
+    /**
39
+     * Factory method that sets up all the necessary components for the creation of the TrackVADEmitter.
40
+     *
41
+     * @param {string} micDeviceId - Target microphone device id.
42
+     * @param {number} procNodeSampleRate - Sample rate of the proc node.
43
+     * @returns {Promise<TrackVADEmitter>} - Promise resolving in a new instance of TrackVADEmitter.
44
+     */
45
+    static create(micDeviceId, procNodeSampleRate, vadProcessor) {
46
+        return RTC.obtainAudioAndVideoPermissions({
47
+            devices: [ 'audio' ],
48
+            micDeviceId
49
+        }).then(localTrack => {
50
+            // We only expect one audio track when specifying a device id.
51
+            if (!localTrack[0]) {
52
+                throw new Error(`Failed to create jitsi local track for device id: ${micDeviceId}`);
53
+            }
54
+
55
+            return new TrackVADEmitter(procNodeSampleRate, vadProcessor, localTrack[0]);
56
+
57
+            // We have no exception handling at this point as there is nothing to clean up, the vadProcessor
58
+            // life cycle is handled by whoever created this instance.
59
+        });
60
+    }
61
+
62
+    /**
63
+     * Sets up the audio graph in the AudioContext.
64
+     *
65
+     * @returns {Promise<void>}
66
+     */
67
+    _initializeAudioContext() {
68
+        this._audioSource = this._audioContext.createMediaStreamSource(this._localTrack.stream);
69
+
70
+        // TODO AudioProcessingNode is deprecated check and replace with alternative.
71
+        // We don't need stereo for determining the VAD score so we create a single channel processing node.
72
+        this._audioProcessingNode = this._audioContext.createScriptProcessor(this._procNodeSampleRate, 1, 1);
73
+        this._audioProcessingNode.onaudioprocess = this._onAudioProcess;
74
+    }
75
+
76
+    /**
77
+     * TODO maybe move this logic to the VAD Processor.
78
+     * ScriptProcessorNode callback, the input parameters contains the PCM audio that is then sent to rnnoise.
79
+     * Rnnoise only accepts PCM samples of 480 bytes whereas the webaudio processor node can't sample at a multiple
80
+     * of 480 thus after each _onAudioProcess callback there will remain and PCM buffer residue equal
81
+     * to _procNodeSampleRate / 480 which will be added to the next sample buffer and so on.
82
+     *
83
+     * @param {AudioProcessingEvent} audioEvent - Audio event.
84
+     * @returns {void}
85
+     */
86
+    _onAudioProcess(audioEvent) {
87
+        // Prepend the residue PCM buffer from the previous process callback.
88
+        const inData = audioEvent.inputBuffer.getChannelData(0);
89
+        const completeInData = [ ...this._bufferResidue, ...inData ];
90
+        const sampleTimestamp = Date.now();
91
+
92
+        let i = 0;
93
+
94
+        for (; i + this._vadSampleSize < completeInData.length; i += this._vadSampleSize) {
95
+            const pcmSample = completeInData.slice(i, i + this._vadSampleSize);
96
+            const vadScore = this._vadProcessor.calculateAudioFrameVAD(pcmSample);
97
+
98
+            this.emit(VAD_SCORE_PUBLISHED, {
99
+                timestamp: sampleTimestamp,
100
+                score: vadScore,
101
+                deviceId: this._micDeviceId
102
+            });
103
+        }
104
+
105
+        this._bufferResidue = completeInData.slice(i, completeInData.length);
106
+    }
107
+
108
+    /**
109
+     * Connects the nodes in the AudioContext to start the flow of audio data.
110
+     *
111
+     * @returns {void}
112
+     */
113
+    _connectAudioGraph() {
114
+        this._audioSource.connect(this._audioProcessingNode);
115
+        this._audioProcessingNode.connect(this._audioContext.destination);
116
+    }
117
+
118
+    /**
119
+     * Disconnects the nodes in the AudioContext.
120
+     *
121
+     * @returns {void}
122
+     */
123
+    _disconnectAudioGraph() {
124
+        // Even thought we disconnect the processing node it seems that some callbacks remain queued,
125
+        // resulting in calls with and uninitialized context.
126
+        // eslint-disable-next-line no-empty-function
127
+        this._audioProcessingNode.onaudioprocess = () => {};
128
+        this._audioProcessingNode.disconnect();
129
+        this._audioSource.disconnect();
130
+    }
131
+
132
+    /**
133
+     * Cleanup potentially acquired resources.
134
+     *
135
+     * @returns {void}
136
+     */
137
+    _cleanupResources() {
138
+        this._disconnectAudioGraph();
139
+        this._localTrack.stopStream();
140
+    }
141
+
142
+    /**
143
+     * Destroy TrackVADEmitter instance (release resources and stop callbacks).
144
+     *
145
+     * @returns {void}
146
+     */
147
+    destroy() {
148
+        if (this._destroyed) {
149
+            return;
150
+        }
151
+
152
+        this._cleanupResources();
153
+        this._destroyed = true;
154
+    }
155
+}

+ 163
- 0
modules/detection/VADTalkMutedDetection.js View File

@@ -0,0 +1,163 @@
1
+import { EventEmitter } from 'events';
2
+import { VAD_SCORE_PUBLISHED, VAD_TALK_WHILE_MUTED } from './DetectionEvents';
3
+import { getLogger } from 'jitsi-meet-logger';
4
+import TrackVADEmitter from '../detection/TrackVADEmitter';
5
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
6
+
7
+const logger = getLogger(__filename);
8
+
9
+/**
10
+ * The value which we use to say, every sound over this threshold
11
+ * is talking on the mic.
12
+ * @type {number}
13
+ */
14
+const VAD_DETECT_THRESHOLD = 0.7;
15
+
16
+/**
17
+ * Detect user trying to speek while is locally muted and fires an event.
18
+ */
19
+export default class VADTalkMutedDetection extends EventEmitter {
20
+    /**
21
+     * Creates TalkMutedDetection
22
+     * @param conference the JitsiConference instance that created us.
23
+     * @param callback the callback to call when detected that the local user is
24
+     * talking while her microphone is muted.
25
+     * @constructor
26
+     */
27
+    constructor(conference, vadProcessor) {
28
+        super();
29
+        logger.info('[ADBG] Created VADTalkMutedDetection.');
30
+
31
+        /**
32
+         * The indicator which determines whether <tt>callback</tt> has been
33
+         * invoked for the current local audio track of <tt>conference</tt> so
34
+         * that it is invoked once only.
35
+         *
36
+         * @private
37
+         */
38
+        this._eventFired = false;
39
+
40
+        this._vadProcessor = vadProcessor;
41
+
42
+        this._vadEmitter = null;
43
+
44
+        this._processing = false;
45
+
46
+        this._scoreArray = [];
47
+
48
+        conference.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, this._trackMuteChanged.bind(this));
49
+        conference.on(JitsiConferenceEvents.TRACK_ADDED, this._trackAdded.bind(this));
50
+
51
+        // TODO do we need to handle the case where tracks are removed, make sure this cleans up properly so
52
+        // we don't have any leeks i.e. stale JitsiLocalTracks
53
+    }
54
+
55
+    /* eslint-disable max-params */
56
+    /**
57
+     * Receives audio level events for all send and receive streams.
58
+     *
59
+     * @param {TraceablePeerConnection} pc - WebRTC PeerConnection object of the
60
+     * @param {number} ssrc - The synchronization source identifier (SSRC) of
61
+     * the endpoint/participant/stream being reported.
62
+     * @param {number} audioLevel - The audio level of <tt>ssrc</tt>.
63
+     * @param {boolean} isLocal - <tt>true</tt> if <tt>ssrc</tt> represents a
64
+     * local/send stream or <tt>false</tt> for a remote/receive stream.
65
+     */
66
+    _processVADScore(vadScore) {
67
+        // We are interested in the local audio stream only and if event is not
68
+        // sent yet.
69
+        if (this._eventFired) {
70
+            return;
71
+        }
72
+
73
+        if (this.audioTrack.isMuted()) {
74
+            if (vadScore.score > 0.8 && !this._processing) {
75
+                this._processing = true;
76
+
77
+                this._processTimeout = setTimeout(() => {
78
+                    let scoreSum = 0;
79
+
80
+                    for (const score of this._scoreArray) {
81
+                        scoreSum += score;
82
+                    }
83
+
84
+                    const avgScore = scoreSum / this._scoreArray.length;
85
+
86
+                    if (avgScore > VAD_DETECT_THRESHOLD) {
87
+                        this.emit(VAD_TALK_WHILE_MUTED, '');
88
+                        this._eventFired = true;
89
+                        console.log('[ADBG] Triggered array size: ', this._scoreArray, '. AVG: ', avgScore);
90
+                    } else {
91
+                        console.log('[ADBG] Not triggered array size: ', this._scoreArray, '. AVG: ', avgScore);
92
+                    }
93
+
94
+                    this._scoreArray = [];
95
+                    this._processing = false;
96
+                }, 1500);
97
+            }
98
+
99
+            if (this._processing) {
100
+                this._scoreArray.push(vadScore.score);
101
+            }
102
+        }
103
+    }
104
+    /* eslint-enable max-params */
105
+
106
+    /**
107
+     * Determines whether a specific {@link JitsiTrack} represents a local audio
108
+     * track.
109
+     *
110
+     * @param {JitsiTrack} track - The <tt>JitsiTrack</tt> to be checked whether
111
+     * it represents a local audio track.
112
+     * @private
113
+     * @return {boolean} - <tt>true</tt> if the specified <tt>track</tt>
114
+     * represents a local audio track; otherwise, <tt>false</tt>.
115
+     */
116
+    _isLocalAudioTrack(track) {
117
+        return track.isAudioTrack() && track.isLocal();
118
+    }
119
+
120
+    /**
121
+     * Notifies this <tt>TalkMutedDetection</tt> that a {@link JitsiTrack} was
122
+     * added to the associated {@link JitsiConference}. Looks for the local
123
+     * audio track only.
124
+     *
125
+     * @param {JitsiTrack} track - The added <tt>JitsiTrack</tt>.
126
+     * @private
127
+     */
128
+    _trackAdded(track) {
129
+        if (this._isLocalAudioTrack(track)) {
130
+            logger.info('[ADBG] Audio track added.');
131
+            this.audioTrack = track;
132
+            this._vadProcessor().then(vadProcessor => {
133
+                TrackVADEmitter.create(track.getDeviceId(), 4096, vadProcessor).then(vadEmitter => {
134
+                    if (this._vadEmitter) {
135
+                        this._vadEmitter.destroy();
136
+                    }
137
+
138
+                    this._vadEmitter = vadEmitter;
139
+                    this._vadEmitter.on(VAD_SCORE_PUBLISHED, this._processVADScore.bind(this));
140
+                    this._eventFired = false;
141
+                    this._processing = false;
142
+                    clearTimeout(this._processTimeout);
143
+                });
144
+            });
145
+        }
146
+    }
147
+
148
+    /**
149
+     * Notifies this <tt>TalkMutedDetection</tt> that the mute state of a
150
+     * {@link JitsiTrack} has changed. Looks for the local audio track only.
151
+     *
152
+     * @param {JitsiTrack} track - The <tt>JitsiTrack</tt> whose mute state has
153
+     * changed.
154
+     * @private
155
+     */
156
+    _trackMuteChanged(track) {
157
+        if (this._isLocalAudioTrack(track) && track.isMuted()) {
158
+            logger.info('[ADBG] Audio track muted.');
159
+            this._eventFired = false;
160
+            clearTimeout(this._processTimeout);
161
+        }
162
+    }
163
+}

+ 2
- 1
package.json View File

@@ -22,6 +22,7 @@
22 22
     "current-executing-script": "0.1.3",
23 23
     "jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
24 24
     "js-utils": "github:jitsi/js-utils#446497893023aa8dec403e0e4e35a22cae6bc87d",
25
+    "lib-jitsi-meet": "^1.0.6",
25 26
     "lodash.isequal": "4.5.0",
26 27
     "sdp-transform": "2.3.0",
27 28
     "strophe.js": "1.2.16",
@@ -56,7 +57,7 @@
56 57
   },
57 58
   "scripts": {
58 59
     "lint": "eslint . && flow",
59
-    "postinstall": "webpack -p",
60
+    "postinstall": "webpack",
60 61
     "test": "karma start karma.conf.js",
61 62
     "test-watch": "karma start karma.conf.js --no-single-run",
62 63
     "validate": "npm ls"

+ 4
- 2
webpack.config.js View File

@@ -61,9 +61,11 @@ const config = {
61 61
         concatenateModules: minimize
62 62
     },
63 63
     output: {
64
-        filename: `[name]${minimize ? '.min' : ''}.js`,
64
+        // eslint-disable-next-line
65
+        filename: `[name]${true ? '.min' : ''}.js`,
65 66
         path: process.cwd(),
66
-        sourceMapFilename: `[name].${minimize ? 'min' : 'js'}.map`
67
+        // eslint-disable-next-line
68
+        sourceMapFilename: `[name].${true ? 'min' : 'js'}.map`
67 69
     },
68 70
     performance: {
69 71
         hints: minimize ? 'error' : false,

Loading…
Cancel
Save