Browse Source

Merge branch 'master' of https://github.com/jitsi/lib-jitsi-meet into callstats-confid

Conflicts:
	JitsiConference.js
	modules/statistics/CallStats.js
dev1
Devin Wilson 9 years ago
parent
commit
f8352fdf4c
74 changed files with 10714 additions and 3570 deletions
  1. 15
    0
      .eslintignore
  2. 34
    0
      .eslintrc.js
  3. 2
    0
      .gitignore
  4. 11
    11
      .jshintrc
  5. 424
    666
      JitsiConference.js
  6. 69
    66
      JitsiConferenceErrors.js
  7. 593
    0
      JitsiConferenceEventManager.js
  8. 161
    139
      JitsiConferenceEvents.js
  9. 29
    8
      JitsiConnection.js
  10. 13
    18
      JitsiConnectionErrors.js
  11. 29
    33
      JitsiConnectionEvents.js
  12. 6
    4
      JitsiMediaDevices.js
  13. 21
    24
      JitsiMediaDevicesEvents.js
  14. 134
    36
      JitsiMeetJS.js
  15. 228
    179
      JitsiParticipant.js
  16. 83
    71
      JitsiTrackError.js
  17. 67
    60
      JitsiTrackErrors.js
  18. 25
    23
      JitsiTrackEvents.js
  19. 15
    9
      connection_optimization/external_connect.js
  20. 33
    5
      doc/API.md
  21. 14
    13
      doc/example/example.js
  22. 79
    43
      modules/RTC/DataChannels.js
  23. 401
    148
      modules/RTC/JitsiLocalTrack.js
  24. 102
    7
      modules/RTC/JitsiRemoteTrack.js
  25. 98
    11
      modules/RTC/JitsiTrack.js
  26. 183
    88
      modules/RTC/RTC.js
  27. 47
    4
      modules/RTC/RTCBrowserType.js
  28. 3
    3
      modules/RTC/RTCUIHelper.js
  29. 168
    56
      modules/RTC/RTCUtils.js
  30. 154
    55
      modules/RTC/ScreenObtainer.js
  31. 3503
    401
      modules/RTC/adapter.screenshare.js
  32. 110
    0
      modules/TalkMutedDetection.js
  33. 453
    0
      modules/connectivity/ConnectionQuality.js
  34. 416
    0
      modules/connectivity/ParticipantConnectionStatus.js
  35. 100
    0
      modules/statistics/AnalyticsAdapter.js
  36. 114
    69
      modules/statistics/CallStats.js
  37. 0
    1
      modules/statistics/LocalStatsCollector.js
  38. 141
    165
      modules/statistics/RTPStatsCollector.js
  39. 117
    50
      modules/statistics/statistics.js
  40. 311
    0
      modules/transcription/audioRecorder.js
  41. 18
    0
      modules/transcription/recordingResult.js
  42. 328
    0
      modules/transcription/transcriber.js
  43. 16
    0
      modules/transcription/transcriberHolder.js
  44. 80
    0
      modules/transcription/transcriptionServices/AbstractTranscriptionService.js
  45. 130
    0
      modules/transcription/transcriptionServices/SphinxTranscriptionService.js
  46. 37
    0
      modules/transcription/word.js
  47. 35
    0
      modules/util/EventEmitterForwarder.js
  48. 30
    2
      modules/util/ScriptUtil.js
  49. 16
    14
      modules/version/ComponentsVersions.js
  50. 168
    67
      modules/xmpp/ChatRoom.js
  51. 11
    0
      modules/xmpp/ConnectionPlugin.js
  52. 23
    10
      modules/xmpp/JingleSession.js
  53. 78
    29
      modules/xmpp/JingleSessionPC.js
  54. 22
    0
      modules/xmpp/JingleSessionState.js
  55. 21
    10
      modules/xmpp/SDP.js
  56. 3
    2
      modules/xmpp/SDPUtil.js
  57. 52
    34
      modules/xmpp/TraceablePeerConnection.js
  58. 19
    7
      modules/xmpp/moderator.js
  59. 5
    4
      modules/xmpp/recording.js
  60. 114
    99
      modules/xmpp/strophe.emuc.js
  61. 243
    222
      modules/xmpp/strophe.jingle.js
  62. 28
    18
      modules/xmpp/strophe.logger.js
  63. 117
    123
      modules/xmpp/strophe.ping.js
  64. 89
    105
      modules/xmpp/strophe.rayo.js
  65. 5
    4
      modules/xmpp/strophe.util.js
  66. 343
    308
      modules/xmpp/xmpp.js
  67. 20
    21
      package.json
  68. 22
    0
      service/RTC/CameraFacingMode.js
  69. 11
    2
      service/RTC/RTCEvents.js
  70. 0
    7
      service/connectionquality/CQEvents.js
  71. 10
    0
      service/connectivity/ConnectionQualityEvents.js
  72. 25
    14
      service/statistics/Events.js
  73. 25
    2
      service/xmpp/XMPPEvents.js
  74. 64
    0
      webpack.config.js

+ 15
- 0
.eslintignore View File

@@ -0,0 +1,15 @@
1
+# The build artifacts of the lib-jiti-meet project.
2
+lib-jitsi-meet.js
3
+lib-jitsi-meet.js.map
4
+lib-jitsi-meet.min.js
5
+lib-jitsi-meet.min.map
6
+
7
+# Third-party source code which we (1) do not want to modify or (2) try to
8
+# modify as little as possible.
9
+libs/*
10
+modules/RTC/adapter.screenshare.js
11
+
12
+# ESLint will by default ignore its own configuration file. However, there does
13
+# not seem to be a reason why we will want to risk being inconsistent with our
14
+# remaining JavaScript source code.
15
+!.eslintrc.js

+ 34
- 0
.eslintrc.js View File

@@ -0,0 +1,34 @@
1
+module.exports = {
2
+    'env': {
3
+        'browser': true,
4
+        'commonjs': true,
5
+        'es6': true
6
+    },
7
+    'extends': 'eslint:recommended',
8
+    'globals': {
9
+        // The globals that (1) are accessed but not defined within many of our
10
+        // files, (2) are certainly defined, and (3) we would like to use
11
+        // without explicitly specifying them (using a comment) inside of our
12
+        // files.
13
+        '__filename': false
14
+    },
15
+    'parserOptions': {
16
+        'sourceType': 'module'
17
+    },
18
+    'rules': {
19
+        'new-cap': [
20
+            'error',
21
+            {
22
+                'capIsNew': false // Behave like JSHint's newcap.
23
+            }
24
+        ],
25
+        // While it is considered a best practice to avoid using methods on
26
+        // console in JavaScript that is designed to be executed in the browser
27
+        // and ESLint includes the rule among its set of recommended rules, (1)
28
+        // the general practice is to strip such calls before pushing to
29
+        // production and (2) we prefer to utilize console in lib-jitsi-meet
30
+        // (and jitsi-meet).
31
+        'no-console': 'off',
32
+        'semi': 'error'
33
+    }
34
+};

+ 2
- 0
.gitignore View File

@@ -6,3 +6,5 @@ node_modules
6 6
 deploy-local.sh
7 7
 .remote-sync.json
8 8
 lib-jitsi-meet.*
9
+npm-*.log
10
+.sync-config.cson

+ 11
- 11
.jshintrc View File

@@ -1,19 +1,19 @@
1 1
 {
2 2
     // Refer to http://jshint.com/docs/options/ for an exhaustive list of options
3 3
     "asi": false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
4
-    "expr": true, // true: Tolerate `ExpressionStatement` as Programs
5
-    "loopfunc": true, // true: Tolerate functions being defined in loops
4
+    "browser": true, // Web Browser (window, document, etc)
6 5
     "curly": false, // true: Require {} for every new block or scope
6
+    "esversion": 6,
7 7
     "evil": true, // true: Tolerate use of `eval` and `new Function()`
8
-    "white": true,
9
-    "undef": true, // true: Require all non-global variables to be declared (prevents global leaks)
10
-    "browser": true, // Web Browser (window, document, etc)
11
-    "node": true, // Node.js
12
-    "trailing": true,
8
+    "expr": true, // true: Tolerate `ExpressionStatement` as Programs
13 9
     "indent": 4, // {int} Number of spaces to use for indentation
14
-    "latedef": true, // true: Require variables/functions to be defined before being used
15
-    "newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()`
16
-    "maxlen": 80, // {int} Max number of characters per line
17 10
     "latedef": false, //This option prohibits the use of a variable before it was defined
18
-    "laxbreak": true //Ignore line breaks around "=", "==", "&&", etc.
11
+    "laxbreak": true, //Ignore line breaks around "=", "==", "&&", etc.
12
+    "loopfunc": true, // true: Tolerate functions being defined in loops
13
+    "maxlen": 80, // {int} Max number of characters per line
14
+    "newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()`
15
+    "node": true, // Node.js
16
+    "trailing": true,
17
+    "undef": true, // true: Require all non-global variables to be declared (prevents global leaks)
18
+    "white": true
19 19
 }

+ 424
- 666
JitsiConference.js
File diff suppressed because it is too large
View File


+ 69
- 66
JitsiConferenceErrors.js View File

@@ -1,68 +1,71 @@
1 1
 /**
2
- * Enumeration with the errors for the conference.
3
- * @type {{string: string}}
4
- */
5
-var JitsiConferenceErrors = {
6
-    /**
7
-     * Indicates that a password is required in order to join the conference.
8
-     */
9
-    PASSWORD_REQUIRED: "conference.passwordRequired",
10
-    /**
11
-     * Indicates that client must be authenticated to create the conference.
12
-     */
13
-    AUTHENTICATION_REQUIRED: "conference.authenticationRequired",
14
-    /**
15
-     * Indicates that password cannot be set for this conference.
16
-     */
17
-    PASSWORD_NOT_SUPPORTED: "conference.passwordNotSupported",
18
-    /**
19
-     * Indicates that a connection error occurred when trying to join a
20
-     * conference.
21
-     */
22
-    CONNECTION_ERROR: "conference.connectionError",
23
-    /**
24
-     * Indicates that the conference setup failed.
25
-     */
26
-    SETUP_FAILED: "conference.setup_failed",
27
-    /**
28
-     * Indicates that there is no available videobridge.
29
-     */
30
-    VIDEOBRIDGE_NOT_AVAILABLE: "conference.videobridgeNotAvailable",
31
-    /**
32
-     * Indicates that reservation system returned error.
33
-     */
34
-    RESERVATION_ERROR: "conference.reservationError",
35
-    /**
36
-     * Indicates that graceful shutdown happened.
37
-     */
38
-    GRACEFUL_SHUTDOWN: "conference.gracefulShutdown",
39
-    /**
40
-     * Indicates that jingle fatal error happened.
41
-     */
42
-    JINGLE_FATAL_ERROR: "conference.jingleFatalError",
43
-    /**
44
-     * Indicates that conference has been destroyed.
45
-     */
46
-    CONFERENCE_DESTROYED: "conference.destroyed",
47
-    /**
48
-     * Indicates that chat error occurred.
49
-     */
50
-    CHAT_ERROR: "conference.chatError",
51
-    /**
52
-     * Indicates that focus error happened.
53
-     */
54
-    FOCUS_DISCONNECTED: "conference.focusDisconnected",
55
-    /**
56
-     * Indicates that focus left the conference.
57
-     */
58
-    FOCUS_LEFT: "conference.focusLeft",
59
-    /**
60
-     * Indicates that max users limit has been reached.
61
-     */
62
-    CONFERENCE_MAX_USERS: "conference.max_users"
63
-    /**
64
-     * Many more errors TBD here.
65
-     */
66
-};
2
+ * The errors for the conference.
3
+ */
67 4
 
68
-module.exports = JitsiConferenceErrors;
5
+/**
6
+ * Indicates that client must be authenticated to create the conference.
7
+ */
8
+export const AUTHENTICATION_REQUIRED = "conference.authenticationRequired";
9
+/**
10
+ * Indicates that chat error occurred.
11
+ */
12
+export const CHAT_ERROR = "conference.chatError";
13
+/**
14
+ * Indicates that conference has been destroyed.
15
+ */
16
+export const CONFERENCE_DESTROYED = "conference.destroyed";
17
+/**
18
+ * Indicates that max users limit has been reached.
19
+ */
20
+export const CONFERENCE_MAX_USERS = "conference.max_users";
21
+/**
22
+ * Indicates that a connection error occurred when trying to join a conference.
23
+ */
24
+export const CONNECTION_ERROR = "conference.connectionError";
25
+/**
26
+ * Indicates that a connection error is due to not allowed,
27
+ * occurred when trying to join a conference.
28
+ */
29
+export const NOT_ALLOWED_ERROR = "conference.connectionError.notAllowed";
30
+/**
31
+ * Indicates that focus error happened.
32
+ */
33
+export const FOCUS_DISCONNECTED = "conference.focusDisconnected";
34
+/**
35
+ * Indicates that focus left the conference.
36
+ */
37
+export const FOCUS_LEFT = "conference.focusLeft";
38
+/**
39
+ * Indicates that graceful shutdown happened.
40
+ */
41
+export const GRACEFUL_SHUTDOWN = "conference.gracefulShutdown";
42
+/**
43
+ * Indicates that the versions of the server side components are incompatible
44
+ * with the client side.
45
+ */
46
+export const INCOMPATIBLE_SERVER_VERSIONS
47
+    = "conference.incompatible_server_versions";
48
+/**
49
+ * Indicates that jingle fatal error happened.
50
+ */
51
+export const JINGLE_FATAL_ERROR = "conference.jingleFatalError";
52
+/**
53
+ * Indicates that password cannot be set for this conference.
54
+ */
55
+export const PASSWORD_NOT_SUPPORTED = "conference.passwordNotSupported";
56
+/**
57
+ * Indicates that a password is required in order to join the conference.
58
+ */
59
+export const PASSWORD_REQUIRED = "conference.passwordRequired";
60
+/**
61
+ * Indicates that reservation system returned error.
62
+ */
63
+export const RESERVATION_ERROR = "conference.reservationError";
64
+/**
65
+ * Indicates that the conference setup failed.
66
+ */
67
+export const SETUP_FAILED = "conference.setup_failed";
68
+/**
69
+ * Indicates that there is no available videobridge.
70
+ */
71
+export const VIDEOBRIDGE_NOT_AVAILABLE = "conference.videobridgeNotAvailable";

+ 593
- 0
JitsiConferenceEventManager.js View File

@@ -0,0 +1,593 @@
1
+/* global Strophe */
2
+var logger = require("jitsi-meet-logger").getLogger(__filename);
3
+var EventEmitterForwarder = require("./modules/util/EventEmitterForwarder");
4
+var XMPPEvents = require("./service/xmpp/XMPPEvents");
5
+var RTCEvents = require("./service/RTC/RTCEvents");
6
+import * as JitsiConferenceErrors from "./JitsiConferenceErrors";
7
+import * as JitsiConferenceEvents from "./JitsiConferenceEvents";
8
+var AuthenticationEvents =
9
+    require("./service/authentication/AuthenticationEvents");
10
+var Statistics = require("./modules/statistics/statistics");
11
+var MediaType = require("./service/RTC/MediaType");
12
+
13
+/**
14
+ * Setups all event listeners related to conference
15
+ * @param conference {JitsiConference} the conference
16
+ */
17
+function JitsiConferenceEventManager(conference) {
18
+    this.conference = conference;
19
+
20
+    //Listeners related to the conference only
21
+    conference.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED,
22
+        function (track) {
23
+            if(!track.isLocal() || !conference.statistics)
24
+                return;
25
+            conference.statistics.sendMuteEvent(track.isMuted(),
26
+                track.getType());
27
+        });
28
+}
29
+
30
+/**
31
+ * Setups event listeners related to conference.chatRoom
32
+ */
33
+JitsiConferenceEventManager.prototype.setupChatRoomListeners = function () {
34
+    var conference = this.conference;
35
+    var chatRoom = conference.room;
36
+    this.chatRoomForwarder = new EventEmitterForwarder(chatRoom,
37
+        this.conference.eventEmitter);
38
+
39
+    chatRoom.addListener(XMPPEvents.ICE_RESTARTING, function () {
40
+        // All data channels have to be closed, before ICE restart
41
+        // otherwise Chrome will not trigger "opened" event for the channel
42
+        // established with the new bridge
43
+        conference.rtc.closeAllDataChannels();
44
+    });
45
+
46
+    chatRoom.addListener(XMPPEvents.REMOTE_TRACK_ADDED,
47
+        function (data) {
48
+            var track = conference.rtc.createRemoteTrack(data);
49
+            if (track) {
50
+                conference.onTrackAdded(track);
51
+            }
52
+        }
53
+    );
54
+    chatRoom.addListener(XMPPEvents.REMOTE_TRACK_REMOVED,
55
+        function (streamId, trackId) {
56
+            conference.getParticipants().forEach(function(participant) {
57
+                var tracks = participant.getTracks();
58
+                for(var i = 0; i < tracks.length; i++) {
59
+                    if(tracks[i]
60
+                        && tracks[i].getStreamId() == streamId
61
+                        && tracks[i].getTrackId() == trackId) {
62
+                        var track = participant._tracks.splice(i, 1)[0];
63
+
64
+                        conference.rtc.removeRemoteTrack(
65
+                            participant.getId(), track.getType());
66
+
67
+                        conference.eventEmitter.emit(
68
+                            JitsiConferenceEvents.TRACK_REMOVED, track);
69
+
70
+                        if(conference.transcriber){
71
+                            conference.transcriber.removeTrack(track);
72
+                        }
73
+
74
+                        return;
75
+                    }
76
+                }
77
+            });
78
+        }
79
+    );
80
+
81
+    chatRoom.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS,
82
+        function (value) {
83
+            // set isMutedByFocus when setAudioMute Promise ends
84
+            conference.rtc.setAudioMute(value).then(
85
+                function() {
86
+                    conference.isMutedByFocus = true;
87
+                },
88
+                function() {
89
+                    logger.warn(
90
+                        "Error while audio muting due to focus request");
91
+                });
92
+        }
93
+    );
94
+
95
+    this.chatRoomForwarder.forward(XMPPEvents.SUBJECT_CHANGED,
96
+        JitsiConferenceEvents.SUBJECT_CHANGED);
97
+
98
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_JOINED,
99
+        JitsiConferenceEvents.CONFERENCE_JOINED);
100
+    // send some analytics events
101
+    chatRoom.addListener(XMPPEvents.MUC_JOINED,
102
+        () => {
103
+            let key, value;
104
+
105
+            this.conference.connectionIsInterrupted = false;
106
+
107
+            for (key in chatRoom.connectionTimes){
108
+                value = chatRoom.connectionTimes[key];
109
+                Statistics.analytics.sendEvent('conference.' + key,
110
+                    {value: value});
111
+            }
112
+            for (key in chatRoom.xmpp.connectionTimes){
113
+                value = chatRoom.xmpp.connectionTimes[key];
114
+                Statistics.analytics.sendEvent('xmpp.' + key,
115
+                    {value: value});
116
+            }
117
+    });
118
+
119
+    this.chatRoomForwarder.forward(XMPPEvents.ROOM_JOIN_ERROR,
120
+        JitsiConferenceEvents.CONFERENCE_FAILED,
121
+        JitsiConferenceErrors.CONNECTION_ERROR);
122
+
123
+    this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_ERROR,
124
+        JitsiConferenceEvents.CONFERENCE_FAILED,
125
+        JitsiConferenceErrors.CONNECTION_ERROR);
126
+    this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR,
127
+        JitsiConferenceEvents.CONFERENCE_FAILED,
128
+        JitsiConferenceErrors.NOT_ALLOWED_ERROR);
129
+
130
+    this.chatRoomForwarder.forward(XMPPEvents.ROOM_MAX_USERS_ERROR,
131
+        JitsiConferenceEvents.CONFERENCE_FAILED,
132
+        JitsiConferenceErrors.CONFERENCE_MAX_USERS);
133
+
134
+    this.chatRoomForwarder.forward(XMPPEvents.PASSWORD_REQUIRED,
135
+        JitsiConferenceEvents.CONFERENCE_FAILED,
136
+        JitsiConferenceErrors.PASSWORD_REQUIRED);
137
+
138
+    this.chatRoomForwarder.forward(XMPPEvents.AUTHENTICATION_REQUIRED,
139
+        JitsiConferenceEvents.CONFERENCE_FAILED,
140
+        JitsiConferenceErrors.AUTHENTICATION_REQUIRED);
141
+
142
+    this.chatRoomForwarder.forward(XMPPEvents.BRIDGE_DOWN,
143
+        JitsiConferenceEvents.CONFERENCE_FAILED,
144
+        JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE);
145
+    chatRoom.addListener(XMPPEvents.BRIDGE_DOWN,
146
+        function (){
147
+            Statistics.analytics.sendEvent('conference.bridgeDown');
148
+        });
149
+
150
+    this.chatRoomForwarder.forward(XMPPEvents.RESERVATION_ERROR,
151
+        JitsiConferenceEvents.CONFERENCE_FAILED,
152
+        JitsiConferenceErrors.RESERVATION_ERROR);
153
+
154
+    this.chatRoomForwarder.forward(XMPPEvents.GRACEFUL_SHUTDOWN,
155
+        JitsiConferenceEvents.CONFERENCE_FAILED,
156
+        JitsiConferenceErrors.GRACEFUL_SHUTDOWN);
157
+
158
+    chatRoom.addListener(XMPPEvents.JINGLE_FATAL_ERROR,
159
+        function (session, error) {
160
+            conference.eventEmitter.emit(
161
+                JitsiConferenceEvents.CONFERENCE_FAILED,
162
+                JitsiConferenceErrors.JINGLE_FATAL_ERROR, error);
163
+        });
164
+
165
+    chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
166
+        function () {
167
+            chatRoom.eventEmitter.emit(
168
+                XMPPEvents.CONFERENCE_SETUP_FAILED,
169
+                new Error("ICE fail"));
170
+        });
171
+
172
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_DESTROYED,
173
+        JitsiConferenceEvents.CONFERENCE_FAILED,
174
+        JitsiConferenceErrors.CONFERENCE_DESTROYED);
175
+
176
+    this.chatRoomForwarder.forward(XMPPEvents.CHAT_ERROR_RECEIVED,
177
+        JitsiConferenceEvents.CONFERENCE_ERROR,
178
+        JitsiConferenceErrors.CHAT_ERROR);
179
+
180
+    this.chatRoomForwarder.forward(XMPPEvents.FOCUS_DISCONNECTED,
181
+        JitsiConferenceEvents.CONFERENCE_FAILED,
182
+        JitsiConferenceErrors.FOCUS_DISCONNECTED);
183
+
184
+    chatRoom.addListener(XMPPEvents.FOCUS_LEFT,
185
+        function () {
186
+            Statistics.analytics.sendEvent('conference.focusLeft');
187
+            conference.eventEmitter.emit(
188
+                JitsiConferenceEvents.CONFERENCE_FAILED,
189
+                JitsiConferenceErrors.FOCUS_LEFT);
190
+        });
191
+
192
+    var eventLogHandler = function (reason) {
193
+        Statistics.sendEventToAll("conference.error." + reason);
194
+    };
195
+    chatRoom.addListener(XMPPEvents.SESSION_ACCEPT_TIMEOUT,
196
+        eventLogHandler.bind(null, "sessionAcceptTimeout"));
197
+
198
+    this.chatRoomForwarder.forward(XMPPEvents.CONNECTION_INTERRUPTED,
199
+        JitsiConferenceEvents.CONNECTION_INTERRUPTED);
200
+    chatRoom.addListener(XMPPEvents.CONNECTION_INTERRUPTED,
201
+        () => {
202
+            Statistics.sendEventToAll('connection.interrupted');
203
+            this.conference.connectionIsInterrupted = true;
204
+        });
205
+
206
+    this.chatRoomForwarder.forward(XMPPEvents.RECORDER_STATE_CHANGED,
207
+        JitsiConferenceEvents.RECORDER_STATE_CHANGED);
208
+
209
+    this.chatRoomForwarder.forward(XMPPEvents.PHONE_NUMBER_CHANGED,
210
+        JitsiConferenceEvents.PHONE_NUMBER_CHANGED);
211
+
212
+    this.chatRoomForwarder.forward(XMPPEvents.CONNECTION_RESTORED,
213
+        JitsiConferenceEvents.CONNECTION_RESTORED);
214
+    chatRoom.addListener(XMPPEvents.CONNECTION_RESTORED,
215
+        () => {
216
+            Statistics.sendEventToAll('connection.restored');
217
+            this.conference.connectionIsInterrupted = false;
218
+        });
219
+
220
+    this.chatRoomForwarder.forward(XMPPEvents.CONFERENCE_SETUP_FAILED,
221
+        JitsiConferenceEvents.CONFERENCE_FAILED,
222
+        JitsiConferenceErrors.SETUP_FAILED);
223
+
224
+    chatRoom.setParticipantPropertyListener(function (node, from) {
225
+        var participant = conference.getParticipantById(from);
226
+        if (!participant) {
227
+            return;
228
+        }
229
+
230
+        participant.setProperty(
231
+            node.tagName.substring("jitsi_participant_".length),
232
+            node.value);
233
+    });
234
+
235
+    this.chatRoomForwarder.forward(XMPPEvents.KICKED,
236
+        JitsiConferenceEvents.KICKED);
237
+    chatRoom.addListener(XMPPEvents.KICKED,
238
+        function () {
239
+            conference.room = null;
240
+            conference.leave.bind(conference);
241
+        });
242
+
243
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_LOCK_CHANGED,
244
+        JitsiConferenceEvents.LOCK_STATE_CHANGED);
245
+
246
+    chatRoom.addListener(XMPPEvents.MUC_MEMBER_JOINED,
247
+        conference.onMemberJoined.bind(conference));
248
+    chatRoom.addListener(XMPPEvents.MUC_MEMBER_LEFT,
249
+        conference.onMemberLeft.bind(conference));
250
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_LEFT,
251
+        JitsiConferenceEvents.CONFERENCE_LEFT);
252
+
253
+    chatRoom.addListener(XMPPEvents.DISPLAY_NAME_CHANGED,
254
+        conference.onDisplayNameChanged.bind(conference));
255
+
256
+    chatRoom.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, function (role) {
257
+        conference.eventEmitter.emit(JitsiConferenceEvents.USER_ROLE_CHANGED,
258
+            conference.myUserId(), role);
259
+
260
+        // log all events for the recorder operated by the moderator
261
+        if (conference.statistics && conference.isModerator()) {
262
+            conference.on(JitsiConferenceEvents.RECORDER_STATE_CHANGED,
263
+                function (status, error) {
264
+                    var logObject = {
265
+                        id: "recorder_status",
266
+                        status: status
267
+                    };
268
+                    if (error) {
269
+                        logObject.error = error;
270
+                    }
271
+                    Statistics.sendLog(JSON.stringify(logObject));
272
+                });
273
+        }
274
+    });
275
+
276
+    chatRoom.addListener(XMPPEvents.MUC_ROLE_CHANGED,
277
+        conference.onUserRoleChanged.bind(conference));
278
+
279
+    chatRoom.addListener(AuthenticationEvents.IDENTITY_UPDATED,
280
+        function (authEnabled, authIdentity) {
281
+            conference.authEnabled = authEnabled;
282
+            conference.authIdentity = authIdentity;
283
+            conference.eventEmitter.emit(
284
+                JitsiConferenceEvents.AUTH_STATUS_CHANGED, authEnabled,
285
+                authIdentity);
286
+        });
287
+
288
+    chatRoom.addListener(XMPPEvents.MESSAGE_RECEIVED,
289
+        function (jid, displayName, txt, myJid, ts) {
290
+            var id = Strophe.getResourceFromJid(jid);
291
+            conference.eventEmitter.emit(JitsiConferenceEvents.MESSAGE_RECEIVED,
292
+                id, txt, ts);
293
+        });
294
+
295
+    chatRoom.addListener(XMPPEvents.PRESENCE_STATUS,
296
+        function (jid, status) {
297
+            var id = Strophe.getResourceFromJid(jid);
298
+            var participant = conference.getParticipantById(id);
299
+            if (!participant || participant._status === status) {
300
+                return;
301
+            }
302
+            participant._status = status;
303
+            conference.eventEmitter.emit(
304
+                JitsiConferenceEvents.USER_STATUS_CHANGED, id, status);
305
+        });
306
+
307
+    conference.room.addListener(XMPPEvents.LOCAL_UFRAG_CHANGED,
308
+        function (ufrag) {
309
+            Statistics.sendLog(
310
+                JSON.stringify({id: "local_ufrag", value: ufrag}));
311
+        });
312
+    conference.room.addListener(XMPPEvents.REMOTE_UFRAG_CHANGED,
313
+        function (ufrag) {
314
+            Statistics.sendLog(
315
+                JSON.stringify({id: "remote_ufrag", value: ufrag}));
316
+        });
317
+
318
+    chatRoom.addPresenceListener("startmuted", function (data, from) {
319
+        var isModerator = false;
320
+        if (conference.myUserId() === from && conference.isModerator()) {
321
+            isModerator = true;
322
+        } else {
323
+            var participant = conference.getParticipantById(from);
324
+            if (participant && participant.isModerator()) {
325
+                isModerator = true;
326
+            }
327
+        }
328
+
329
+        if (!isModerator) {
330
+            return;
331
+        }
332
+
333
+        var startAudioMuted = data.attributes.audio === 'true';
334
+        var startVideoMuted = data.attributes.video === 'true';
335
+
336
+        var updated = false;
337
+
338
+        if (startAudioMuted !== conference.startMutedPolicy.audio) {
339
+            conference.startMutedPolicy.audio = startAudioMuted;
340
+            updated = true;
341
+        }
342
+
343
+        if (startVideoMuted !== conference.startMutedPolicy.video) {
344
+            conference.startMutedPolicy.video = startVideoMuted;
345
+            updated = true;
346
+        }
347
+
348
+        if (updated) {
349
+            conference.eventEmitter.emit(
350
+                JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
351
+                conference.startMutedPolicy
352
+            );
353
+        }
354
+    });
355
+
356
+    chatRoom.addPresenceListener("videomuted", function (values, from) {
357
+        conference.rtc.handleRemoteTrackMute(MediaType.VIDEO,
358
+            values.value == "true", from);
359
+    });
360
+
361
+    chatRoom.addPresenceListener("audiomuted", function (values, from) {
362
+        conference.rtc.handleRemoteTrackMute(MediaType.AUDIO,
363
+            values.value == "true", from);
364
+    });
365
+
366
+    chatRoom.addPresenceListener("videoType", function(data, from) {
367
+        conference.rtc.handleRemoteTrackVideoTypeChanged(data.value, from);
368
+    });
369
+
370
+    chatRoom.addPresenceListener("devices", function (data, from) {
371
+        var isAudioAvailable = false;
372
+        var isVideoAvailable = false;
373
+        data.children.forEach(function (config) {
374
+            if (config.tagName === 'audio') {
375
+                isAudioAvailable = config.value === 'true';
376
+            }
377
+            if (config.tagName === 'video') {
378
+                isVideoAvailable = config.value === 'true';
379
+            }
380
+        });
381
+
382
+        var availableDevices;
383
+        if (conference.myUserId() === from) {
384
+            availableDevices = conference.availableDevices;
385
+        } else {
386
+            var participant = conference.getParticipantById(from);
387
+            if (!participant) {
388
+                return;
389
+            }
390
+
391
+            availableDevices = participant._availableDevices;
392
+        }
393
+
394
+        var updated = false;
395
+
396
+        if (availableDevices.audio !== isAudioAvailable) {
397
+            updated = true;
398
+            availableDevices.audio = isAudioAvailable;
399
+        }
400
+
401
+        if (availableDevices.video !== isVideoAvailable) {
402
+            updated = true;
403
+            availableDevices.video = isVideoAvailable;
404
+        }
405
+
406
+        if (updated) {
407
+            conference.eventEmitter.emit(
408
+                JitsiConferenceEvents.AVAILABLE_DEVICES_CHANGED,
409
+                from, availableDevices);
410
+        }
411
+    });
412
+
413
+    if(conference.statistics) {
414
+        chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
415
+            function (pc) {
416
+                conference.statistics.sendIceConnectionFailedEvent(pc);
417
+            });
418
+
419
+        chatRoom.addListener(XMPPEvents.CREATE_OFFER_FAILED,
420
+            function (e, pc) {
421
+                conference.statistics.sendCreateOfferFailed(e, pc);
422
+            });
423
+
424
+        chatRoom.addListener(XMPPEvents.CREATE_ANSWER_FAILED,
425
+            function (e, pc) {
426
+                conference.statistics.sendCreateAnswerFailed(e, pc);
427
+            });
428
+
429
+        chatRoom.addListener(XMPPEvents.SET_LOCAL_DESCRIPTION_FAILED,
430
+            function (e, pc) {
431
+                conference.statistics.sendSetLocalDescFailed(e, pc);
432
+            });
433
+
434
+        chatRoom.addListener(XMPPEvents.SET_REMOTE_DESCRIPTION_FAILED,
435
+            function (e, pc) {
436
+                conference.statistics.sendSetRemoteDescFailed(e, pc);
437
+            });
438
+
439
+        chatRoom.addListener(XMPPEvents.ADD_ICE_CANDIDATE_FAILED,
440
+            function (e, pc) {
441
+                conference.statistics.sendAddIceCandidateFailed(e, pc);
442
+            });
443
+    }
444
+};
445
+
446
+/**
447
+ * Setups event listeners related to conference.rtc
448
+ */
449
+JitsiConferenceEventManager.prototype.setupRTCListeners = function () {
450
+    var conference = this.conference;
451
+
452
+    this.rtcForwarder = new EventEmitterForwarder(conference.rtc,
453
+        this.conference.eventEmitter);
454
+
455
+    conference.rtc.addListener(RTCEvents.DOMINANT_SPEAKER_CHANGED,
456
+        function (id) {
457
+            if(conference.lastDominantSpeaker !== id && conference.room) {
458
+                conference.lastDominantSpeaker = id;
459
+                conference.eventEmitter.emit(
460
+                    JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id);
461
+            }
462
+            if (conference.statistics && conference.myUserId() === id) {
463
+                // We are the new dominant speaker.
464
+                conference.statistics.sendDominantSpeakerEvent();
465
+            }
466
+        });
467
+
468
+    conference.rtc.addListener(RTCEvents.DATA_CHANNEL_OPEN, function () {
469
+        var now = window.performance.now();
470
+        logger.log("(TIME) data channel opened ", now);
471
+        conference.room.connectionTimes["data.channel.opened"] = now;
472
+        Statistics.analytics.sendEvent('conference.dataChannel.open',
473
+            {value: now});
474
+    });
475
+
476
+    this.rtcForwarder.forward(RTCEvents.LASTN_CHANGED,
477
+        JitsiConferenceEvents.IN_LAST_N_CHANGED);
478
+
479
+    this.rtcForwarder.forward(RTCEvents.LASTN_ENDPOINT_CHANGED,
480
+        JitsiConferenceEvents.LAST_N_ENDPOINTS_CHANGED);
481
+
482
+    conference.rtc.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED,
483
+        function (devices) {
484
+            conference.room.updateDeviceAvailability(devices);
485
+        });
486
+
487
+    conference.rtc.addListener(RTCEvents.ENDPOINT_MESSAGE_RECEIVED,
488
+        function (from, payload) {
489
+            const participant = conference.getParticipantById(from);
490
+            if (participant) {
491
+                conference.eventEmitter.emit(
492
+                    JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
493
+                    participant, payload);
494
+            } else {
495
+                logger.warn(
496
+                    "Ignored ENDPOINT_MESSAGE_RECEIVED " +
497
+                    "for not existing participant: " + from, payload);
498
+            }
499
+        });
500
+};
501
+
502
+/**
503
+ * Setups event listeners related to conference.xmpp
504
+ */
505
+JitsiConferenceEventManager.prototype.setupXMPPListeners = function () {
506
+    var conference = this.conference;
507
+    conference.xmpp.addListener(
508
+        XMPPEvents.CALL_INCOMING, conference.onIncomingCall.bind(conference));
509
+    conference.xmpp.addListener(
510
+        XMPPEvents.CALL_ENDED, conference.onCallEnded.bind(conference));
511
+
512
+    conference.xmpp.addListener(XMPPEvents.START_MUTED_FROM_FOCUS,
513
+        function (audioMuted, videoMuted) {
514
+            conference.startAudioMuted = audioMuted;
515
+            conference.startVideoMuted = videoMuted;
516
+
517
+            // mute existing local tracks because this is initial mute from
518
+            // Jicofo
519
+            conference.getLocalTracks().forEach(function (track) {
520
+                switch (track.getType()) {
521
+                    case MediaType.AUDIO:
522
+                        conference.startAudioMuted && track.mute();
523
+                        break;
524
+                    case MediaType.VIDEO:
525
+                        conference.startVideoMuted && track.mute();
526
+                        break;
527
+                }
528
+            });
529
+
530
+            conference.eventEmitter.emit(JitsiConferenceEvents.STARTED_MUTED);
531
+        });
532
+};
533
+
534
+/**
535
+ * Setups event listeners related to conference.statistics
536
+ */
537
+JitsiConferenceEventManager.prototype.setupStatisticsListeners = function () {
538
+    var conference = this.conference;
539
+    if(!conference.statistics)
540
+        return;
541
+
542
+    conference.statistics.addAudioLevelListener(function (ssrc, level) {
543
+        var resource = conference.rtc.getResourceBySSRC(ssrc);
544
+        if (!resource)
545
+            return;
546
+
547
+        conference.rtc.setAudioLevel(resource, level);
548
+    });
549
+    conference.statistics.addConnectionStatsListener(function (stats) {
550
+        var ssrc2resolution = stats.resolution;
551
+
552
+        var id2resolution = {};
553
+
554
+        // preprocess resolutions: group by user id, skip incorrect
555
+        // resolutions etc.
556
+        Object.keys(ssrc2resolution).forEach(function (ssrc) {
557
+            var resolution = ssrc2resolution[ssrc];
558
+
559
+            if (!resolution.width || !resolution.height ||
560
+                resolution.width == -1 || resolution.height == -1) {
561
+                return;
562
+            }
563
+
564
+            var id = conference.rtc.getResourceBySSRC(ssrc);
565
+            if (!id) {
566
+                return;
567
+            }
568
+
569
+            // ssrc to resolution map for user id
570
+            var idResolutions = id2resolution[id] || {};
571
+            idResolutions[ssrc] = resolution;
572
+
573
+            id2resolution[id] = idResolutions;
574
+        });
575
+
576
+        stats.resolution = id2resolution;
577
+
578
+        conference.eventEmitter.emit(
579
+            JitsiConferenceEvents.CONNECTION_STATS, stats);
580
+    });
581
+
582
+    conference.statistics.addByteSentStatsListener(function (stats) {
583
+        conference.getLocalTracks().forEach(function (track) {
584
+            var ssrc = track.getSSRC();
585
+            if(!track.isAudioTrack() || !ssrc || !stats.hasOwnProperty(ssrc))
586
+                return;
587
+
588
+            track._setByteSent(stats[ssrc]);
589
+        });
590
+    });
591
+};
592
+
593
+module.exports = JitsiConferenceEventManager;

+ 161
- 139
JitsiConferenceEvents.js View File

@@ -1,141 +1,163 @@
1 1
 /**
2
- * Enumeration with the events for the conference.
3
- * @type {{string: string}}
4
- */
5
-var JitsiConferenceEvents = {
6
-    /**
7
-     * A new media track was added to the conference. The event provides the
8
-     * following parameters to its listeners:
9
-     *
10
-     * @param {JitsiTrack} track the added JitsiTrack
11
-     */
12
-    TRACK_ADDED: "conference.trackAdded",
13
-    /**
14
-     * The media track was removed from the conference. The event provides the
15
-     * following parameters to its listeners:
16
-     *
17
-     * @param {JitsiTrack} track the removed JitsiTrack
18
-     */
19
-    TRACK_REMOVED: "conference.trackRemoved",
20
-    /**
21
-     * The dominant speaker was changed.
22
-     */
23
-    DOMINANT_SPEAKER_CHANGED: "conference.dominantSpeaker",
24
-    /**
25
-     * A new user joinned the conference.
26
-     */
27
-    USER_JOINED: "conference.userJoined",
28
-    /**
29
-     * A user has left the conference.
30
-     */
31
-    USER_LEFT: "conference.userLeft",
32
-    /**
33
-     * User role changed.
34
-     */
35
-    USER_ROLE_CHANGED: "conference.roleChanged",
36
-    /**
37
-     * User status changed.
38
-     */
39
-    USER_STATUS_CHANGED: "conference.statusChanged",
40
-    /**
41
-     * New text message was received.
42
-     */
43
-    MESSAGE_RECEIVED: "conference.messageReceived",
44
-    /**
45
-     * A user has changed it display name
46
-     */
47
-    DISPLAY_NAME_CHANGED: "conference.displayNameChanged",
48
-    /**
49
-     * Indicates that subject of the conference has changed.
50
-     */
51
-    SUBJECT_CHANGED: "conference.subjectChanged",
52
-    /**
53
-     * A participant avatar has changed.
54
-     */
55
-    AVATAR_CHANGED: "conference.avatarChanged",
56
-    /**
57
-     * New local connection statistics are received.
58
-     */
59
-    CONNECTION_STATS: "conference.connectionStats",
60
-    /**
61
-     * The Last N set is changed.
62
-     */
63
-    LAST_N_ENDPOINTS_CHANGED: "conference.lastNEndpointsChanged",
64
-    /**
65
-     * You are included / excluded in somebody's last N set
66
-     */
67
-    IN_LAST_N_CHANGED: "conference.inLastNChanged",
68
-    /**
69
-     * A media track ( attached to the conference) mute status was changed.
70
-     */
71
-    TRACK_MUTE_CHANGED: "conference.trackMuteChanged",
72
-    /**
73
-     * Audio levels of a media track ( attached to the conference) was changed.
74
-     */
75
-    TRACK_AUDIO_LEVEL_CHANGED: "conference.audioLevelsChanged",
76
-    /**
77
-     * Indicates that the connection to the conference has been interrupted
78
-     * for some reason.
79
-     */
80
-    CONNECTION_INTERRUPTED: "conference.connectionInterrupted",
81
-    /**
82
-     * Indicates that the connection to the conference has been restored.
83
-     */
84
-    CONNECTION_RESTORED: "conference.connectionRestored",
85
-    /**
86
-     * Indicates that conference failed.
87
-     */
88
-    CONFERENCE_FAILED: "conference.failed",
89
-    /**
90
-     * Indicates that an error occured.
91
-     */
92
-    CONFERENCE_ERROR: "conference.error",
93
-    /**
94
-     * Indicates that conference has been joined. The event does NOT provide any
95
-     * parameters to its listeners.
96
-     */
97
-    CONFERENCE_JOINED: "conference.joined",
98
-    /**
99
-     * Indicates that conference has been left.
100
-     */
101
-    CONFERENCE_LEFT: "conference.left",
102
-    /**
103
-     * You are kicked from the conference.
104
-     */
105
-    KICKED: "conferenece.kicked",
106
-    /**
107
-     * Indicates that start muted settings changed.
108
-     */
109
-    START_MUTED_POLICY_CHANGED: "conference.start_muted_policy_changed",
110
-    /**
111
-     * Indicates that the local user has started muted.
112
-     */
113
-    STARTED_MUTED: "conference.started_muted",
114
-    /**
115
-     * Indicates that DTMF support changed.
116
-     */
117
-    DTMF_SUPPORT_CHANGED: "conference.dtmfSupportChanged",
118
-    /**
119
-     * Indicates that recording state changed.
120
-     */
121
-    RECORDER_STATE_CHANGED: "conference.recorderStateChanged",
122
-    /**
123
-     * Indicates that phone number changed.
124
-     */
125
-    PHONE_NUMBER_CHANGED: "conference.phoneNumberChanged",
126
-    /**
127
-     * Indicates that available devices changed.
128
-     */
129
-    AVAILABLE_DEVICES_CHANGED: "conference.availableDevicesChanged",
130
-    /**
131
-     * Indicates that authentication status changed.
132
-     */
133
-    AUTH_STATUS_CHANGED: "conference.auth_status_changed",
134
-    /**
135
-     * Indicates that a the value of a specific property of a specific
136
-     * participant has changed.
137
-     */
138
-    PARTICIPANT_PROPERTY_CHANGED: "conference.participant_property_changed"
139
-};
2
+ * The events for the conference.
3
+ */
140 4
 
141
-module.exports = JitsiConferenceEvents;
5
+/**
6
+ * Indicates that authentication status changed.
7
+ */
8
+export const AUTH_STATUS_CHANGED = "conference.auth_status_changed";
9
+/**
10
+ * Indicates that available devices changed.
11
+ */
12
+export const AVAILABLE_DEVICES_CHANGED = "conference.availableDevicesChanged";
13
+/**
14
+ * A participant avatar has changed.
15
+ */
16
+export const AVATAR_CHANGED = "conference.avatarChanged";
17
+/**
18
+ * Indicates that an error occured.
19
+ */
20
+export const CONFERENCE_ERROR = "conference.error";
21
+/**
22
+ * Indicates that conference failed.
23
+ */
24
+export const CONFERENCE_FAILED = "conference.failed";
25
+/**
26
+ * Indicates that conference has been joined. The event does NOT provide any
27
+ * parameters to its listeners.
28
+ */
29
+export const CONFERENCE_JOINED = "conference.joined";
30
+/**
31
+ * Indicates that conference has been left.
32
+ */
33
+export const CONFERENCE_LEFT = "conference.left";
34
+/**
35
+ * Indicates that the connection to the conference has been interrupted for some
36
+ * reason.
37
+ */
38
+export const CONNECTION_INTERRUPTED = "conference.connectionInterrupted";
39
+/**
40
+ * Indicates that the connection to the conference has been restored.
41
+ */
42
+export const CONNECTION_RESTORED = "conference.connectionRestored";
43
+/**
44
+ * New local connection statistics are received.
45
+ * @deprecated Use ConnectionQualityEvents.LOCAL_STATS_UPDATED instead.
46
+ */
47
+export const CONNECTION_STATS = "conference.connectionStats";
48
+/**
49
+ * A user has changed it display name
50
+ */
51
+export const DISPLAY_NAME_CHANGED = "conference.displayNameChanged";
52
+/**
53
+ * The dominant speaker was changed.
54
+ */
55
+export const DOMINANT_SPEAKER_CHANGED = "conference.dominantSpeaker";
56
+/**
57
+ * Indicates that DTMF support changed.
58
+ */
59
+export const DTMF_SUPPORT_CHANGED = "conference.dtmfSupportChanged";
60
+/**
61
+ * Indicates that a message from another participant is received on data
62
+ * channel.
63
+ */
64
+export const ENDPOINT_MESSAGE_RECEIVED = "conference.endpoint_message_received";
65
+/**
66
+ * You are included / excluded in somebody's last N set
67
+ */
68
+export const IN_LAST_N_CHANGED = "conference.inLastNChanged";
69
+/**
70
+ * You are kicked from the conference.
71
+ */
72
+export const KICKED = "conferenece.kicked";
73
+/**
74
+ * The Last N set is changed.
75
+ */
76
+export const LAST_N_ENDPOINTS_CHANGED = "conference.lastNEndpointsChanged";
77
+/**
78
+ * Indicates that the room has been locked or unlocked.
79
+ */
80
+export const LOCK_STATE_CHANGED = "conference.lock_state_changed";
81
+/**
82
+ * New text message was received.
83
+ */
84
+export const MESSAGE_RECEIVED = "conference.messageReceived";
85
+/**
86
+ * Event fired when JVB sends notification about interrupted/restored user's
87
+ * ICE connection status. First argument is the ID of the participant and
88
+ * the seconds is a boolean indicating if the connection is currently
89
+ * active(true = active, false = interrupted).
90
+ * The current status value can be obtained by calling
91
+ * JitsiParticipant.isConnectionActive().
92
+ */
93
+export const PARTICIPANT_CONN_STATUS_CHANGED
94
+    = "conference.participant_conn_status_changed";
95
+/**
96
+ * Indicates that a the value of a specific property of a specific participant
97
+ * has changed.
98
+ */
99
+export const PARTICIPANT_PROPERTY_CHANGED
100
+    = "conference.participant_property_changed";
101
+/**
102
+ * Indicates that phone number changed.
103
+ */
104
+export const PHONE_NUMBER_CHANGED = "conference.phoneNumberChanged";
105
+/**
106
+ * Indicates that recording state changed.
107
+ */
108
+export const RECORDER_STATE_CHANGED = "conference.recorderStateChanged";
109
+/**
110
+ * Indicates that start muted settings changed.
111
+ */
112
+export const START_MUTED_POLICY_CHANGED
113
+    = "conference.start_muted_policy_changed";
114
+/**
115
+ * Indicates that the local user has started muted.
116
+ */
117
+export const STARTED_MUTED = "conference.started_muted";
118
+/**
119
+ * Indicates that subject of the conference has changed.
120
+ */
121
+export const SUBJECT_CHANGED = "conference.subjectChanged";
122
+/**
123
+ * Event indicates that local user is talking while he muted himself
124
+ */
125
+export const TALK_WHILE_MUTED = "conference.talk_while_muted";
126
+/**
127
+ * A new media track was added to the conference. The event provides the
128
+ * following parameters to its listeners:
129
+ *
130
+ * @param {JitsiTrack} track the added JitsiTrack
131
+ */
132
+export const TRACK_ADDED = "conference.trackAdded";
133
+/**
134
+ * Audio levels of a media track ( attached to the conference) was changed.
135
+ */
136
+export const TRACK_AUDIO_LEVEL_CHANGED = "conference.audioLevelsChanged";
137
+/**
138
+ * A media track ( attached to the conference) mute status was changed.
139
+ */
140
+export const TRACK_MUTE_CHANGED = "conference.trackMuteChanged";
141
+/**
142
+ * The media track was removed from the conference. The event provides the
143
+ * following parameters to its listeners:
144
+ *
145
+ * @param {JitsiTrack} track the removed JitsiTrack
146
+ */
147
+export const TRACK_REMOVED = "conference.trackRemoved";
148
+/**
149
+ * A new user joinned the conference.
150
+ */
151
+export const USER_JOINED = "conference.userJoined";
152
+/**
153
+ * A user has left the conference.
154
+ */
155
+export const USER_LEFT = "conference.userLeft";
156
+/**
157
+ * User role changed.
158
+ */
159
+export const USER_ROLE_CHANGED = "conference.roleChanged";
160
+/**
161
+ * User status changed.
162
+ */
163
+export const USER_STATUS_CHANGED = "conference.statusChanged";

+ 29
- 8
JitsiConnection.js View File

@@ -1,5 +1,7 @@
1 1
 var JitsiConference = require("./JitsiConference");
2
-var XMPP = require("./modules/xmpp/xmpp");
2
+import * as JitsiConnectionEvents from "./JitsiConnectionEvents";
3
+import XMPP from "./modules/xmpp/xmpp";
4
+var Statistics = require("./modules/statistics/statistics");
3 5
 
4 6
 /**
5 7
  * Creates new connection object for the Jitsi Meet server side video conferencing service. Provides access to the
@@ -15,6 +17,25 @@ function JitsiConnection(appID, token, options) {
15 17
     this.options = options;
16 18
     this.xmpp = new XMPP(options, token);
17 19
     this.conferences = {};
20
+
21
+    this.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED,
22
+        function (errType, msg) {
23
+            // sends analytics and callstats event
24
+            Statistics.sendEventToAll('connection.failed.' + errType,
25
+                {label: msg});
26
+        }.bind(this));
27
+
28
+    this.addEventListener(JitsiConnectionEvents.CONNECTION_DISCONNECTED,
29
+        function (msg) {
30
+            // we can see disconnects from normal tab closing of the browser
31
+            // and then there are no msgs, but we want to log only disconnects
32
+            // when there is real error
33
+            if(msg)
34
+                Statistics.analytics.sendEvent(
35
+                    'connection.disconnected.' + msg);
36
+            Statistics.sendLog(
37
+                JSON.stringify({id: "connection.disconnected", msg: msg}));
38
+        });
18 39
 }
19 40
 
20 41
 /**
@@ -27,7 +48,7 @@ JitsiConnection.prototype.connect = function (options) {
27 48
         options = {};
28 49
 
29 50
     this.xmpp.connect(options.id, options.password);
30
-}
51
+};
31 52
 
32 53
 /**
33 54
  * Attach to existing connection. Can be used for optimizations. For example:
@@ -38,7 +59,7 @@ JitsiConnection.prototype.connect = function (options) {
38 59
  */
39 60
 JitsiConnection.prototype.attach = function (options) {
40 61
     this.xmpp.attach(options);
41
-}
62
+};
42 63
 
43 64
 /**
44 65
  * Disconnect the client from the server.
@@ -51,7 +72,7 @@ JitsiConnection.prototype.disconnect = function () {
51 72
     var x = this.xmpp;
52 73
 
53 74
     x.disconnect.apply(x, arguments);
54
-}
75
+};
55 76
 
56 77
 /**
57 78
  * This method allows renewal of the tokens if they are expiring.
@@ -59,7 +80,7 @@ JitsiConnection.prototype.disconnect = function () {
59 80
  */
60 81
 JitsiConnection.prototype.setToken = function (token) {
61 82
     this.token = token;
62
-}
83
+};
63 84
 
64 85
 /**
65 86
  * Creates and joins new conference.
@@ -74,7 +95,7 @@ JitsiConnection.prototype.initJitsiConference = function (name, options) {
74 95
         = new JitsiConference({name: name, config: options, connection: this});
75 96
     this.conferences[name] = conference;
76 97
     return conference;
77
-}
98
+};
78 99
 
79 100
 /**
80 101
  * Subscribes the passed listener to the event.
@@ -83,7 +104,7 @@ JitsiConnection.prototype.initJitsiConference = function (name, options) {
83 104
  */
84 105
 JitsiConnection.prototype.addEventListener = function (event, listener) {
85 106
     this.xmpp.addListener(event, listener);
86
-}
107
+};
87 108
 
88 109
 /**
89 110
  * Unsubscribes the passed handler.
@@ -92,7 +113,7 @@ JitsiConnection.prototype.addEventListener = function (event, listener) {
92 113
  */
93 114
 JitsiConnection.prototype.removeEventListener = function (event, listener) {
94 115
     this.xmpp.removeListener(event, listener);
95
-}
116
+};
96 117
 
97 118
 /**
98 119
  * Returns measured connectionTimes.

+ 13
- 18
JitsiConnectionErrors.js View File

@@ -1,21 +1,16 @@
1 1
 /**
2
- * Enumeration with the errors for the connection.
3
- * @type {{string: string}}
2
+ * The errors for the connection.
4 3
  */
5
-var JitsiConnectionErrors = {
6
-    /**
7
-     * Indicates that a password is required in order to join the conference.
8
-     */
9
-    PASSWORD_REQUIRED: "connection.passwordRequired",
10
-    /**
11
-     * Indicates that a connection error occurred when trying to join a
12
-     * conference.
13
-     */
14
-    CONNECTION_ERROR: "connection.connectionError",
15
-    /**
16
-     * Not specified errors.
17
-     */
18
-    OTHER_ERROR: "connection.otherError"
19
-};
20 4
 
21
-module.exports = JitsiConnectionErrors;
5
+/**
6
+ * Indicates that a connection error occurred when trying to join a conference.
7
+ */
8
+export const CONNECTION_ERROR = "connection.connectionError";
9
+/**
10
+ * Not specified errors.
11
+ */
12
+export const OTHER_ERROR = "connection.otherError";
13
+/**
14
+ * Indicates that a password is required in order to join the conference.
15
+ */
16
+export const PASSWORD_REQUIRED = "connection.passwordRequired";

+ 29
- 33
JitsiConnectionEvents.js View File

@@ -1,36 +1,32 @@
1 1
 /**
2
- * Enumeration with the events for the connection.
3
- * @type {{string: string}}
2
+ * The events for the connection.
4 3
  */
5
-var JitsiConnnectionEvents = {
6
-    /**
7
-     * Indicates that the connection has been failed for some reason. The event
8
-     * proivdes the following parameters to its listeners:
9
-     *
10
-     * @param err {string} the error (message) associated with the failure
11
-     */
12
-    CONNECTION_FAILED: "connection.connectionFailed",
13
-    /**
14
-     * Indicates that the connection has been established. The event provides
15
-     * the following parameters to its listeners:
16
-     *
17
-     * @param id {string} the ID of the local endpoint/participant/peer (within
18
-     * the context of the established connection)
19
-     */
20
-    CONNECTION_ESTABLISHED: "connection.connectionEstablished",
21
-    /**
22
-     * Indicates that the connection has been disconnected. The event provides
23
-     * the following parameters to its listeners:
24
-     *
25
-     * @param msg {string} a message associated with the disconnect such as the
26
-     * last (known) error message
27
-     */
28
-    CONNECTION_DISCONNECTED: "connection.connectionDisconnected",
29
-    /**
30
-     * Indicates that the perfomed action cannot be executed because the
31
-     * connection is not in the correct state(connected, disconnected, etc.)
32
-     */
33
-    WRONG_STATE: "connection.wrongState"
34
-};
35 4
 
36
-module.exports = JitsiConnnectionEvents;
5
+/**
6
+ * Indicates that the connection has been disconnected. The event provides
7
+ * the following parameters to its listeners:
8
+ *
9
+ * @param msg {string} a message associated with the disconnect such as the
10
+ * last (known) error message
11
+ */
12
+export const CONNECTION_DISCONNECTED = "connection.connectionDisconnected";
13
+/**
14
+ * Indicates that the connection has been established. The event provides
15
+ * the following parameters to its listeners:
16
+ *
17
+ * @param id {string} the ID of the local endpoint/participant/peer (within
18
+ * the context of the established connection)
19
+ */
20
+export const CONNECTION_ESTABLISHED = "connection.connectionEstablished";
21
+/**
22
+ * Indicates that the connection has been failed for some reason. The event
23
+ * provides the following parameters to its listeners:
24
+ *
25
+ * @param err {string} the error (message) associated with the failure
26
+ */
27
+export const CONNECTION_FAILED = "connection.connectionFailed";
28
+/**
29
+ * Indicates that the performed action cannot be executed because the
30
+ * connection is not in the correct state(connected, disconnected, etc.)
31
+ */
32
+export const WRONG_STATE = "connection.wrongState";

+ 6
- 4
JitsiMediaDevices.js View File

@@ -2,7 +2,7 @@ var EventEmitter = require("events");
2 2
 var RTCEvents = require('./service/RTC/RTCEvents');
3 3
 var RTC = require("./modules/RTC/RTC");
4 4
 var MediaType = require('./service/RTC/MediaType');
5
-var JitsiMediaDevicesEvents = require('./JitsiMediaDevicesEvents');
5
+import * as JitsiMediaDevicesEvents from "./JitsiMediaDevicesEvents";
6 6
 var Statistics = require("./modules/statistics/statistics");
7 7
 
8 8
 var eventEmitter = new EventEmitter();
@@ -46,7 +46,9 @@ var JitsiMediaDevices = {
46 46
     },
47 47
     /**
48 48
      * Checks if its possible to enumerate available cameras/micropones.
49
-     * @returns {boolean} true if available, false otherwise.
49
+     * @returns {Promise<boolean>} a Promise which will be resolved only once
50
+     * the WebRTC stack is ready, either with true if the device listing is
51
+     * available available or with false otherwise.
50 52
      */
51 53
     isDeviceListAvailable: function () {
52 54
         return RTC.isDeviceListAvailable();
@@ -128,9 +130,9 @@ var JitsiMediaDevices = {
128 130
      * Emits an event.
129 131
      * @param {string} event - event name
130 132
      */
131
-    emitEvent: function (event) {
133
+    emitEvent: function (event) { // eslint-disable-line no-unused-vars
132 134
         eventEmitter.emit.apply(eventEmitter, arguments);
133 135
     }
134 136
 };
135 137
 
136
-module.exports = JitsiMediaDevices;
138
+module.exports = JitsiMediaDevices;

+ 21
- 24
JitsiMediaDevicesEvents.js View File

@@ -1,27 +1,24 @@
1 1
 /**
2
- * Enumeration with the events for the media devices.
3
- * @type {{string: string}}
2
+ * The events for the media devices.
4 3
  */
5
-var JitsiMediaDevicesEvents = {
6
-    /**
7
-     * Indicates that the list of available media devices has been changed. The
8
-     * event provides the following parameters to its listeners:
9
-     *
10
-     * @param {MediaDeviceInfo[]} devices - array of MediaDeviceInfo or
11
-     *  MediaDeviceInfo-like objects that are currently connected.
12
-     *  @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
13
-     */
14
-    DEVICE_LIST_CHANGED: "mediaDevices.devicechange",
15
-    /**
16
-     * Indicates that the environment is currently showing permission prompt to
17
-     * access camera and/or microphone. The event provides the following
18
-     * parameters to its listeners:
19
-     *
20
-     * @param {'chrome'|'opera'|'firefox'|'iexplorer'|'safari'|'nwjs'
21
-     *      |'react-native'|'android'} environmentType - type of browser or
22
-     *      other execution environment.
23
-     */
24
-    PERMISSION_PROMPT_IS_SHOWN: "mediaDevices.permissionPromptIsShown"
25
-};
26 4
 
27
-module.exports = JitsiMediaDevicesEvents;
5
+/**
6
+ * Indicates that the list of available media devices has been changed. The
7
+ * event provides the following parameters to its listeners:
8
+ *
9
+ * @param {MediaDeviceInfo[]} devices - array of MediaDeviceInfo or
10
+ *  MediaDeviceInfo-like objects that are currently connected.
11
+ *  @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
12
+ */
13
+export const DEVICE_LIST_CHANGED = "mediaDevices.devicechange";
14
+/**
15
+ * Indicates that the environment is currently showing permission prompt to
16
+ * access camera and/or microphone. The event provides the following
17
+ * parameters to its listeners:
18
+ *
19
+ * @param {'chrome'|'opera'|'firefox'|'iexplorer'|'safari'|'nwjs'
20
+ *  |'react-native'|'android'} environmentType - type of browser or
21
+ *  other execution environment.
22
+ */
23
+export const PERMISSION_PROMPT_IS_SHOWN
24
+    = "mediaDevices.permissionPromptIsShown";

+ 134
- 36
JitsiMeetJS.js View File

@@ -2,14 +2,15 @@ var logger = require("jitsi-meet-logger").getLogger(__filename);
2 2
 var AuthUtil = require("./modules/util/AuthUtil");
3 3
 var JitsiConnection = require("./JitsiConnection");
4 4
 var JitsiMediaDevices = require("./JitsiMediaDevices");
5
-var JitsiConferenceEvents = require("./JitsiConferenceEvents");
6
-var JitsiConnectionEvents = require("./JitsiConnectionEvents");
7
-var JitsiMediaDevicesEvents = require('./JitsiMediaDevicesEvents');
8
-var JitsiConnectionErrors = require("./JitsiConnectionErrors");
9
-var JitsiConferenceErrors = require("./JitsiConferenceErrors");
10
-var JitsiTrackEvents = require("./JitsiTrackEvents");
11
-var JitsiTrackErrors = require("./JitsiTrackErrors");
12
-var JitsiTrackError = require("./JitsiTrackError");
5
+import * as JitsiConferenceErrors from "./JitsiConferenceErrors";
6
+import * as JitsiConferenceEvents from "./JitsiConferenceEvents";
7
+import * as JitsiConnectionErrors from "./JitsiConnectionErrors";
8
+import * as JitsiConnectionEvents from "./JitsiConnectionEvents";
9
+import * as JitsiMediaDevicesEvents from "./JitsiMediaDevicesEvents";
10
+import * as ConnectionQualityEvents from "./service/connectivity/ConnectionQualityEvents";
11
+import JitsiTrackError from "./JitsiTrackError";
12
+import * as JitsiTrackErrors from "./JitsiTrackErrors";
13
+import * as JitsiTrackEvents from "./JitsiTrackEvents";
13 14
 var JitsiRecorderErrors = require("./JitsiRecorderErrors");
14 15
 var Logger = require("jitsi-meet-logger");
15 16
 var MediaType = require("./service/RTC/MediaType");
@@ -31,7 +32,7 @@ function getLowerResolution(resolution) {
31 32
     var order = Resolutions[resolution].order;
32 33
     var res = null;
33 34
     var resName = null;
34
-    for(var i in Resolutions) {
35
+    for(let i in Resolutions) {
35 36
         var tmp = Resolutions[i];
36 37
         if (!res || (res.order < tmp.order && tmp.order < order)) {
37 38
             resName = i;
@@ -41,6 +42,29 @@ function getLowerResolution(resolution) {
41 42
     return resName;
42 43
 }
43 44
 
45
+/**
46
+ * Checks the available devices in options and concatenate the data to the
47
+ * name, which will be used as analytics event name. Adds resolution for the
48
+ * devices.
49
+ * @param name name of event
50
+ * @param options gum options
51
+ * @returns {*}
52
+ */
53
+function addDeviceTypeToAnalyticsEvent(name, options) {
54
+    if (options.devices.indexOf("audio") !== -1) {
55
+        name += ".audio";
56
+    }
57
+    if (options.devices.indexOf("desktop") !== -1) {
58
+        name += ".desktop";
59
+    }
60
+    if (options.devices.indexOf("video") !== -1) {
61
+        // we have video add resolution
62
+        name += ".video." + options.resolution;
63
+    }
64
+
65
+    return name;
66
+}
67
+
44 68
 /**
45 69
  * Namespace for the interface of Jitsi Meet Library.
46 70
  */
@@ -53,7 +77,8 @@ var LibJitsiMeet = {
53 77
         conference: JitsiConferenceEvents,
54 78
         connection: JitsiConnectionEvents,
55 79
         track: JitsiTrackEvents,
56
-        mediaDevices: JitsiMediaDevicesEvents
80
+        mediaDevices: JitsiMediaDevicesEvents,
81
+        connectionQuality: ConnectionQualityEvents
57 82
     },
58 83
     errors: {
59 84
         conference: JitsiConferenceErrors,
@@ -66,28 +91,39 @@ var LibJitsiMeet = {
66 91
     },
67 92
     logLevels: Logger.levels,
68 93
     mediaDevices: JitsiMediaDevices,
94
+    analytics: null,
69 95
     init: function (options) {
70
-        Statistics.audioLevelsEnabled = !options.disableAudioLevels;
71
-
72
-        if(typeof options.audioLevelsInterval === 'number') {
73
-            Statistics.audioLevelsInterval = options.audioLevelsInterval;
74
-        }
96
+        let logObject, attr;
97
+        Statistics.init(options);
98
+        this.analytics = Statistics.analytics;
75 99
 
76 100
         if (options.enableWindowOnErrorHandler) {
77 101
             GlobalOnErrorHandler.addHandler(
78 102
                 this.getGlobalOnErrorHandler.bind(this));
79 103
         }
80 104
 
81
-        // Lets send some general stats useful for debugging problems
105
+        // Log deployment-specific information, if available.
82 106
         if (window.jitsiRegionInfo
83 107
             && Object.keys(window.jitsiRegionInfo).length > 0) {
84
-            // remove quotes to make it prettier
85
-            Statistics.sendLog(
86
-                JSON.stringify(window.jitsiRegionInfo).replace(/\"/g, ""));
108
+            logObject = {};
109
+            for (attr in window.jitsiRegionInfo) {
110
+                if (window.jitsiRegionInfo.hasOwnProperty(attr)) {
111
+                    logObject[attr] = window.jitsiRegionInfo[attr];
112
+                }
113
+            }
114
+
115
+            logObject.id = "deployment_info";
116
+            Statistics.sendLog(JSON.stringify(logObject));
87 117
         }
88 118
 
89
-        if(this.version)
90
-            Statistics.sendLog("LibJitsiMeet:" + this.version);
119
+        if(this.version) {
120
+            logObject = {
121
+                id: "component_version",
122
+                component: "lib-jitsi-meet",
123
+                version: this.version
124
+            };
125
+            Statistics.sendLog(JSON.stringify(logObject));
126
+        }
91 127
 
92 128
         return RTC.init(options || {});
93 129
     },
@@ -112,6 +148,28 @@ var LibJitsiMeet = {
112 148
      * will be returned trough the Promise, otherwise JitsiTrack objects will be returned.
113 149
      * @param {string} options.cameraDeviceId
114 150
      * @param {string} options.micDeviceId
151
+     * @param {object} options.desktopSharingExtensionExternalInstallation -
152
+     * enables external installation process for desktop sharing extension if
153
+     * the inline installation is not posible. The following properties should
154
+     * be provided:
155
+     * @param {intiger} interval - the interval (in ms) for
156
+     * checking whether the desktop sharing extension is installed or not
157
+     * @param {Function} checkAgain - returns boolean. While checkAgain()==true
158
+     * createLocalTracks will wait and check on every "interval" ms for the
159
+     * extension. If the desktop extension is not install and checkAgain()==true
160
+     * createLocalTracks will finish with rejected Promise.
161
+     * @param {Function} listener - The listener will be called to notify the
162
+     * user of lib-jitsi-meet that createLocalTracks is starting external
163
+     * extension installation process.
164
+     * NOTE: If the inline installation process is not possible and external
165
+     * installation is enabled the listener property will be called to notify
166
+     * the start of external installation process. After that createLocalTracks
167
+     * will start to check for the extension on every interval ms until the
168
+     * plugin is installed or until checkAgain return false. If the extension
169
+     * is found createLocalTracks will try to get the desktop sharing track and
170
+     * will finish the execution. If checkAgain returns false, createLocalTracks
171
+     * will finish the execution with rejected Promise.
172
+     *
115 173
      * @param {boolean} (firePermissionPromptIsShownEvent) - if event
116 174
      *      JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN should be fired
117 175
      * @returns {Promise.<{Array.<JitsiTrack>}, JitsiConferenceError>}
@@ -124,27 +182,31 @@ var LibJitsiMeet = {
124 182
         if (firePermissionPromptIsShownEvent === true) {
125 183
             window.setTimeout(function () {
126 184
                 if (!promiseFulfilled) {
127
-                    var browser = RTCBrowserType.getBrowserType()
128
-                        .split('rtc_browser.')[1];
129
-
130
-                    if (RTCBrowserType.isAndroid()) {
131
-                        browser = 'android';
132
-                    }
133
-
134 185
                     JitsiMediaDevices.emitEvent(
135 186
                         JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
136
-                        browser);
187
+                        RTCBrowserType.getBrowserName());
137 188
                 }
138 189
             }, USER_MEDIA_PERMISSION_PROMPT_TIMEOUT);
139 190
         }
140 191
 
192
+        if(!window.connectionTimes)
193
+            window.connectionTimes = {};
194
+        window.connectionTimes["obtainPermissions.start"] =
195
+            window.performance.now();
196
+
141 197
         return RTC.obtainAudioAndVideoPermissions(options || {})
142 198
             .then(function(tracks) {
143 199
                 promiseFulfilled = true;
144 200
 
201
+                window.connectionTimes["obtainPermissions.end"] =
202
+                    window.performance.now();
203
+
204
+                Statistics.analytics.sendEvent(addDeviceTypeToAnalyticsEvent(
205
+                    "getUserMedia.success", options), {value: options});
206
+
145 207
                 if(!RTC.options.disableAudioLevels)
146
-                    for(var i = 0; i < tracks.length; i++) {
147
-                        var track = tracks[i];
208
+                    for(let i = 0; i < tracks.length; i++) {
209
+                        const track = tracks[i];
148 210
                         var mStream = track.getOriginalStream();
149 211
                         if(track.getType() === MediaType.AUDIO){
150 212
                             Statistics.startLocalStats(mStream,
@@ -157,6 +219,17 @@ var LibJitsiMeet = {
157 219
                         }
158 220
                     }
159 221
 
222
+                // set real device ids
223
+                var currentlyAvailableMediaDevices
224
+                    = RTC.getCurrentlyAvailableMediaDevices();
225
+                if (currentlyAvailableMediaDevices) {
226
+                    for(let i = 0; i < tracks.length; i++) {
227
+                        const track = tracks[i];
228
+                        track._setRealDeviceIdFromDeviceList(
229
+                            currentlyAvailableMediaDevices);
230
+                    }
231
+                }
232
+
160 233
                 return tracks;
161 234
             }).catch(function (error) {
162 235
                 promiseFulfilled = true;
@@ -171,6 +244,9 @@ var LibJitsiMeet = {
171 244
                         logger.debug("Retry createLocalTracks with resolution",
172 245
                             newResolution);
173 246
 
247
+                        Statistics.analytics.sendEvent(
248
+                            "getUserMedia.fail.resolution." + oldResolution);
249
+
174 250
                         return LibJitsiMeet.createLocalTracks(options);
175 251
                     }
176 252
                 }
@@ -180,18 +256,43 @@ var LibJitsiMeet = {
180 256
                     // User cancelled action is not really an error, so only
181 257
                     // log it as an event to avoid having conference classified
182 258
                     // as partially failed
183
-                    Statistics.sendLog(error.message);
259
+                    const logObject = {
260
+                        id: "chrome_extension_user_canceled",
261
+                        message: error.message
262
+                    };
263
+                    Statistics.sendLog(JSON.stringify(logObject));
264
+                    Statistics.analytics.sendEvent(
265
+                        "getUserMedia.userCancel.extensionInstall");
266
+                } else if (JitsiTrackErrors.NOT_FOUND === error.name) {
267
+                    // logs not found devices with just application log to cs
268
+                    const logObject = {
269
+                        id: "usermedia_missing_device",
270
+                        status: error.gum.devices
271
+                    };
272
+                    Statistics.sendLog(JSON.stringify(logObject));
273
+                    Statistics.analytics.sendEvent(
274
+                        "getUserMedia.deviceNotFound."
275
+                            + error.gum.devices.join('.'));
184 276
                 } else {
185 277
                     // Report gUM failed to the stats
186 278
                     Statistics.sendGetUserMediaFailed(error);
279
+                    Statistics.analytics.sendEvent(
280
+                        addDeviceTypeToAnalyticsEvent(
281
+                            "getUserMedia.failed", options) + '.' + error.name,
282
+                        {value: options});
187 283
                 }
188 284
 
285
+                window.connectionTimes["obtainPermissions.end"] =
286
+                    window.performance.now();
287
+
189 288
                 return Promise.reject(error);
190 289
             }.bind(this));
191 290
     },
192 291
     /**
193 292
      * Checks if its possible to enumerate available cameras/micropones.
194
-     * @returns {boolean} true if available, false otherwise.
293
+     * @returns {Promise<boolean>} a Promise which will be resolved only once
294
+     * the WebRTC stack is ready, either with true if the device listing is
295
+     * available available or with false otherwise.
195 296
      * @deprecated use JitsiMeetJS.mediaDevices.isDeviceListAvailable instead
196 297
      */
197 298
     isDeviceListAvailable: function () {
@@ -249,7 +350,4 @@ var LibJitsiMeet = {
249 350
     }
250 351
 };
251 352
 
252
-//Setups the promise object.
253
-window.Promise = window.Promise || require("es6-promise").Promise;
254
-
255 353
 module.exports = LibJitsiMeet;

+ 228
- 179
JitsiParticipant.js View File

@@ -1,211 +1,260 @@
1 1
 /* global Strophe */
2
-var JitsiConferenceEvents = require('./JitsiConferenceEvents');
2
+import * as JitsiConferenceEvents from "./JitsiConferenceEvents";
3
+import * as MediaType from "./service/RTC/MediaType";
3 4
 
4 5
 /**
5
- * Represents a participant in (a member of) a conference.
6
- * @param jid the conference XMPP jid
7
- * @param conference
8
- * @param displayName
9
- * @param isHidden indicates if this participant is a hidden participant
6
+ * Represents a participant in (i.e. a member of) a conference.
10 7
  */
11
-function JitsiParticipant(jid, conference, displayName, isHidden){
12
-    this._jid = jid;
13
-    this._id = Strophe.getResourceFromJid(jid);
14
-    this._conference = conference;
15
-    this._displayName = displayName;
16
-    this._supportsDTMF = false;
17
-    this._tracks = [];
18
-    this._role = 'none';
19
-    this._status = null;
20
-    this._availableDevices = {
21
-        audio: undefined,
22
-        video: undefined
23
-    };
24
-    this._isHidden = isHidden;
25
-    this._properties = {};
26
-}
27
-
28
-/**
29
- * @returns {JitsiConference} The conference that this participant belongs to.
30
- */
31
-JitsiParticipant.prototype.getConference = function() {
32
-    return this._conference;
33
-};
34
-
35
-/**
36
- * Gets the value of a property of this participant.
37
- */
38
-JitsiParticipant.prototype.getProperty = function(name) {
39
-    return this._properties[name];
40
-};
41
-
42
-/**
43
- * Sets the value of a property of this participant, and fires an event if the
44
- * value has changed.
45
- * @name the name of the property.
46
- * @value the value to set.
47
- */
48
-JitsiParticipant.prototype.setProperty = function(name, value) {
49
-    var oldValue = this._properties[name];
50
-    this._properties[name] = value;
51
-
52
-    if (value !== oldValue) {
53
-        this._conference.eventEmitter.emit(
54
-            JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
55
-            this,
56
-            name,
57
-            oldValue,
58
-            value);
8
+export default class JitsiParticipant {
9
+    /**
10
+     * Initializes a new JitsiParticipant instance.
11
+     *
12
+     * @constructor
13
+     * @param jid the conference XMPP jid
14
+     * @param conference
15
+     * @param displayName
16
+     * @param {Boolean} hidden - True if the new JitsiParticipant instance is to
17
+     * represent a hidden participant; otherwise, false.
18
+     */
19
+    constructor(jid, conference, displayName, hidden) {
20
+        this._jid = jid;
21
+        this._id = Strophe.getResourceFromJid(jid);
22
+        this._conference = conference;
23
+        this._displayName = displayName;
24
+        this._supportsDTMF = false;
25
+        this._tracks = [];
26
+        this._role = 'none';
27
+        this._status = null;
28
+        this._availableDevices = {
29
+            audio: undefined,
30
+            video: undefined
31
+        };
32
+        this._hidden = hidden;
33
+        this._isConnectionActive = true;
34
+        this._properties = {};
59 35
     }
60
-};
61
-
62
-/**
63
- * @returns {Array.<JitsiTrack>} The list of media tracks for this participant.
64
- */
65
-JitsiParticipant.prototype.getTracks = function() {
66
-    return this._tracks.slice();
67
-};
68 36
 
69
-/**
70
- * @returns {String} The ID of this participant.
71
- */
72
-JitsiParticipant.prototype.getId = function() {
73
-    return this._id;
74
-};
75
-
76
-/**
77
- * @returns {String} The JID of this participant.
78
- */
79
-JitsiParticipant.prototype.getJid = function() {
80
-    return this._jid;
81
-};
37
+    /**
38
+     * @returns {JitsiConference} The conference that this participant belongs
39
+     * to.
40
+     */
41
+    getConference() {
42
+        return this._conference;
43
+    }
82 44
 
83
-/**
84
- * @returns {String} The human-readable display name of this participant.
85
- */
86
-JitsiParticipant.prototype.getDisplayName = function() {
87
-    return this._displayName;
88
-};
45
+    /**
46
+     * Gets the value of a property of this participant.
47
+     */
48
+    getProperty(name) {
49
+        return this._properties[name];
50
+    }
89 51
 
90
-/**
91
- * @returns {String} The status of the participant.
92
- */
93
-JitsiParticipant.prototype.getStatus = function () {
94
-    return this._status;
95
-};
52
+    /**
53
+     * Checks whether this <tt>JitsiParticipant</tt> has any video tracks which
54
+     * are muted according to their underlying WebRTC <tt>MediaStreamTrack</tt>
55
+     * muted status.
56
+     * @return {boolean} <tt>true</tt> if this <tt>participant</tt> contains any
57
+     * video <tt>JitsiTrack</tt>s which are muted as defined in
58
+     * {@link JitsiTrack.isWebRTCTrackMuted}.
59
+     */
60
+    hasAnyVideoTrackWebRTCMuted() {
61
+        return this.getTracks().some(function(jitsiTrack) {
62
+            return jitsiTrack.getType() === MediaType.VIDEO
63
+                && jitsiTrack.isWebRTCTrackMuted();
64
+        });
65
+    }
96 66
 
97
-/**
98
- * @returns {Boolean} Whether this participant is a moderator or not.
99
- */
100
-JitsiParticipant.prototype.isModerator = function() {
101
-    return this._role === 'moderator';
102
-};
67
+    /**
68
+     * Updates participant's connection status.
69
+     * @param {boolean} isActive true if the user's connection is fine or false
70
+     * when the user is having connectivity issues.
71
+     * @private
72
+     */
73
+    _setIsConnectionActive(isActive) {
74
+        this._isConnectionActive = isActive;
75
+    }
103 76
 
104
-/**
105
- * @returns {Boolean} Whether this participant is a hidden participant. Some
106
- * special system participants may want to join hidden (like for example the
107
- * recorder).
108
- */
109
-JitsiParticipant.prototype.isHidden = function() {
110
-    return this._isHidden;
111
-};
77
+    /**
78
+     * Checks participant's connectivity status.
79
+     *
80
+     * @returns {boolean} true if the connection is currently ok or false when
81
+     * the user is having connectivity issues.
82
+     */
83
+    isConnectionActive() {
84
+        return this._isConnectionActive;
85
+    }
112 86
 
113
-// Gets a link to an etherpad instance advertised by the participant?
114
-//JitsiParticipant.prototype.getEtherpad = function() {
115
-//
116
-//}
87
+    /**
88
+     * Sets the value of a property of this participant, and fires an event if
89
+     * the value has changed.
90
+     * @name the name of the property.
91
+     * @value the value to set.
92
+     */
93
+    setProperty(name, value) {
94
+        var oldValue = this._properties[name];
95
+
96
+        if (value !== oldValue) {
97
+            this._properties[name] = value;
98
+            this._conference.eventEmitter.emit(
99
+                JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
100
+                this,
101
+                name,
102
+                oldValue,
103
+                value);
104
+        }
105
+    }
117 106
 
107
+    /**
108
+     * @returns {Array.<JitsiTrack>} The list of media tracks for this
109
+     * participant.
110
+     */
111
+    getTracks() {
112
+        return this._tracks.slice();
113
+    }
118 114
 
119
-/*
120
- * @returns {Boolean} Whether this participant has muted their audio.
121
- */
122
-JitsiParticipant.prototype.isAudioMuted = function() {
123
-    return this.getTracks().reduce(function (track, isAudioMuted) {
124
-        return isAudioMuted && (track.isVideoTrack() || track.isMuted());
125
-    }, true);
126
-};
127
-
128
-/*
129
- * @returns {Boolean} Whether this participant has muted their video.
130
- */
131
-JitsiParticipant.prototype.isVideoMuted = function() {
132
-    return this.getTracks().reduce(function (track, isVideoMuted) {
133
-        return isVideoMuted && (track.isAudioTrack() || track.isMuted());
134
-    }, true);
135
-};
136
-
137
-/*
138
- * @returns {???} The latest statistics reported by this participant
139
- * (i.e. info used to populate the GSM bars)
140
- * TODO: do we expose this or handle it internally?
141
- */
142
-JitsiParticipant.prototype.getLatestStats = function() {
115
+    /**
116
+     * @returns {String} The ID of this participant.
117
+     */
118
+    getId() {
119
+        return this._id;
120
+    }
143 121
 
144
-};
122
+    /**
123
+     * @returns {String} The JID of this participant.
124
+     */
125
+    getJid() {
126
+        return this._jid;
127
+    }
145 128
 
146
-/**
147
- * @returns {String} The role of this participant.
148
- */
149
-JitsiParticipant.prototype.getRole = function() {
150
-    return this._role;
151
-};
129
+    /**
130
+     * @returns {String} The human-readable display name of this participant.
131
+     */
132
+    getDisplayName() {
133
+        return this._displayName;
134
+    }
152 135
 
153
-/*
154
- * @returns {Boolean} Whether this participant is
155
- * the conference focus (i.e. jicofo).
156
- */
157
-JitsiParticipant.prototype.isFocus = function() {
136
+    /**
137
+     * @returns {String} The status of the participant.
138
+     */
139
+    getStatus () {
140
+        return this._status;
141
+    }
158 142
 
159
-};
143
+    /**
144
+     * @returns {Boolean} Whether this participant is a moderator or not.
145
+     */
146
+    isModerator() {
147
+        return this._role === 'moderator';
148
+    }
160 149
 
161
-/*
162
- * @returns {Boolean} Whether this participant is
163
- * a conference recorder (i.e. jirecon).
164
- */
165
-JitsiParticipant.prototype.isRecorder = function() {
150
+    /**
151
+     * @returns {Boolean} Whether this participant is a hidden participant. Some
152
+     * special system participants may want to join hidden (like for example the
153
+     * recorder).
154
+     */
155
+    isHidden() {
156
+        return this._hidden;
157
+    }
166 158
 
167
-};
159
+    // Gets a link to an etherpad instance advertised by the participant?
160
+    //getEtherpad() {
161
+    //}
168 162
 
169
-/*
170
- * @returns {Boolean} Whether this participant is a SIP gateway (i.e. jigasi).
171
- */
172
-JitsiParticipant.prototype.isSipGateway = function() {
163
+    /**
164
+     * @returns {Boolean} Whether this participant has muted their audio.
165
+     */
166
+    isAudioMuted() {
167
+        return this._isMediaTypeMuted(MediaType.AUDIO);
168
+    }
173 169
 
174
-};
170
+    /**
171
+     * Determines whether all JitsiTracks which are of a specific MediaType and
172
+     * which belong to this JitsiParticipant are muted.
173
+     *
174
+     * @param {MediaType} mediaType - The MediaType of the JitsiTracks to be
175
+     * checked.
176
+     * @private
177
+     * @returns {Boolean} True if all JitsiTracks which are of the specified
178
+     * mediaType and which belong to this JitsiParticipant are muted; otherwise,
179
+     * false.
180
+     */
181
+    _isMediaTypeMuted(mediaType) {
182
+        return this.getTracks().reduce(
183
+            (muted, track) =>
184
+                muted && (track.getType() !== mediaType || track.isMuted()),
185
+            true);
186
+    }
175 187
 
176
-/**
177
- * @returns {Boolean} Whether this participant
178
- * is currently sharing their screen.
179
- */
180
-JitsiParticipant.prototype.isScreenSharing = function() {
188
+    /**
189
+     * @returns {Boolean} Whether this participant has muted their video.
190
+     */
191
+    isVideoMuted() {
192
+        return this._isMediaTypeMuted(MediaType.VIDEO);
193
+    }
181 194
 
182
-};
195
+    /**
196
+     * @returns {???} The latest statistics reported by this participant (i.e.
197
+     * info used to populate the GSM bars)
198
+     * TODO: do we expose this or handle it internally?
199
+     */
200
+    getLatestStats() {
201
+    }
183 202
 
184
-/**
185
- * @returns {String} The user agent of this participant
186
- * (i.e. browser userAgent string).
187
- */
188
-JitsiParticipant.prototype.getUserAgent = function() {
203
+    /**
204
+     * @returns {String} The role of this participant.
205
+     */
206
+    getRole() {
207
+        return this._role;
208
+    }
189 209
 
190
-};
210
+    /**
211
+     * @returns {Boolean} Whether this participant is the conference focus (i.e.
212
+     * jicofo).
213
+     */
214
+    isFocus() {
215
+    }
191 216
 
192
-/**
193
- * Kicks the participant from the conference (requires certain privileges).
194
- */
195
-JitsiParticipant.prototype.kick = function() {
217
+    /**
218
+     * @returns {Boolean} Whether this participant is a conference recorder
219
+     * (i.e. jirecon).
220
+     */
221
+    isRecorder() {
222
+    }
196 223
 
197
-};
224
+    /**
225
+     * @returns {Boolean} Whether this participant is a SIP gateway (i.e.
226
+     * jigasi).
227
+     */
228
+    isSipGateway() {
229
+    }
198 230
 
199
-/**
200
- * Asks this participant to mute themselves.
201
- */
202
-JitsiParticipant.prototype.askToMute = function() {
231
+    /**
232
+     * @returns {Boolean} Whether this participant is currently sharing their
233
+     * screen.
234
+     */
235
+    isScreenSharing() {
236
+    }
203 237
 
204
-};
238
+    /**
239
+     * @returns {String} The user agent of this participant (i.e. browser
240
+     * userAgent string).
241
+     */
242
+    getUserAgent() {
243
+    }
205 244
 
206
-JitsiParticipant.prototype.supportsDTMF = function () {
207
-    return this._supportsDTMF;
208
-};
245
+    /**
246
+     * Kicks the participant from the conference (requires certain privileges).
247
+     */
248
+    kick() {
249
+    }
209 250
 
251
+    /**
252
+     * Asks this participant to mute themselves.
253
+     */
254
+    askToMute() {
255
+    }
210 256
 
211
-module.exports = JitsiParticipant;
257
+    supportsDTMF() {
258
+        return this._supportsDTMF;
259
+    }
260
+}

+ 83
- 71
JitsiTrackError.js View File

@@ -1,6 +1,6 @@
1
-var JitsiTrackErrors = require("./JitsiTrackErrors");
1
+import * as JitsiTrackErrors from "./JitsiTrackErrors";
2 2
 
3
-var TRACK_ERROR_TO_MESSAGE_MAP = {};
3
+const TRACK_ERROR_TO_MESSAGE_MAP = {};
4 4
 
5 5
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
6 6
     = "Video resolution is not supported: ";
@@ -22,103 +22,116 @@ TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.CONSTRAINT_FAILED]
22 22
     = "Constraint could not be satisfied: ";
23 23
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_IS_DISPOSED]
24 24
     = "Track has been already disposed";
25
+TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_NO_STREAM_FOUND]
26
+    = "Track does not have an associated Media Stream";
25 27
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_MUTE_UNMUTE_IN_PROGRESS]
26 28
     = "Track mute/unmute process is currently in progress";
29
+TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
30
+    = "The track has stopped receiving data from it's source";
27 31
 
28 32
 /**
29
- * Object representing error that happened to a JitsiTrack. Can represent
30
- * various types of errors. For error descriptions (@see JitsiTrackErrors).
31
- * @constructor
33
+ * Represents an error that occurred to a JitsiTrack. Can represent various
34
+ * types of errors. For error descriptions (@see JitsiTrackErrors).
35
+ *
32 36
  * @extends Error
33
- * @param {Object|string} error - error object or error name
34
- * @param {Object|string} (options) - getUserMedia constraints object or error
35
- *      message
36
- * @param {('audio'|'video'|'desktop'|'screen'|'audiooutput')[]} (devices) -
37
- *      list of getUserMedia requested devices
38 37
  */
39
-function JitsiTrackError(error, options, devices) {
40
-    if (typeof error === "object" && typeof error.name !== "undefined") {
41
-        /**
42
-         * Additional information about original getUserMedia error
43
-         * and constraints.
44
-         * @type {{
45
-         *          error: Object,
46
-         *          constraints: Object,
47
-         *          devices: Array.<'audio'|'video'|'desktop'|'screen'>
48
-         *      }}
49
-         */
50
-        this.gum = {
51
-            error: error,
52
-            constraints: options,
53
-            devices: devices && Array.isArray(devices)
54
-                ? devices.slice(0)
55
-                : undefined
56
-        };
38
+export default class JitsiTrackError extends Error {
39
+    /**
40
+     * Initializes a new JitsiTrackError instance.
41
+     *
42
+     * @constructor
43
+     * @param {Object|string} error - error object or error name
44
+     * @param {Object|string} (options) - getUserMedia constraints object or
45
+     * error message
46
+     * @param {('audio'|'video'|'desktop'|'screen'|'audiooutput')[]} (devices) -
47
+     * list of getUserMedia requested devices
48
+     */
49
+    constructor(error, options, devices) {
50
+        super();
57 51
 
58
-        switch (error.name) {
52
+        if (typeof error === "object" && typeof error.name !== "undefined") {
53
+            /**
54
+             * Additional information about original getUserMedia error
55
+             * and constraints.
56
+             * @type {{
57
+             *     error: Object,
58
+             *     constraints: Object,
59
+             *     devices: Array.<'audio'|'video'|'desktop'|'screen'>
60
+             * }}
61
+             */
62
+            this.gum = {
63
+                error,
64
+                constraints: options,
65
+                devices: devices && Array.isArray(devices)
66
+                    ? devices.slice(0)
67
+                    : undefined
68
+            };
69
+
70
+            switch (error.name) {
59 71
             case "PermissionDeniedError":
60 72
             case "SecurityError":
61 73
                 this.name = JitsiTrackErrors.PERMISSION_DENIED;
62
-                this.message = TRACK_ERROR_TO_MESSAGE_MAP[
63
-                        JitsiTrackErrors.PERMISSION_DENIED]
74
+                this.message
75
+                    = TRACK_ERROR_TO_MESSAGE_MAP[this.name]
64 76
                         + (this.gum.devices || []).join(", ");
65 77
                 break;
78
+            case "DevicesNotFoundError":
66 79
             case "NotFoundError":
67 80
                 this.name = JitsiTrackErrors.NOT_FOUND;
68
-                this.message = TRACK_ERROR_TO_MESSAGE_MAP[
69
-                        JitsiTrackErrors.NOT_FOUND]
81
+                this.message
82
+                    = TRACK_ERROR_TO_MESSAGE_MAP[this.name]
70 83
                         + (this.gum.devices || []).join(", ");
71 84
                 break;
72 85
             case "ConstraintNotSatisfiedError":
73 86
             case "OverconstrainedError":
74 87
                 var constraintName = error.constraintName;
75 88
 
76
-                if (options && options.video
77
-                    && (devices || []).indexOf('video') > -1
78
-                    &&
79
-                    (constraintName === "minWidth" ||
80
-                        constraintName === "maxWidth" ||
81
-                        constraintName === "minHeight" ||
82
-                        constraintName === "maxHeight" ||
83
-                        constraintName === "width" ||
84
-                        constraintName === "height")) {
89
+                if (options
90
+                        && options.video
91
+                        && (!devices || devices.indexOf('video') > -1)
92
+                        && (constraintName === "minWidth"
93
+                            || constraintName === "maxWidth"
94
+                            || constraintName === "minHeight"
95
+                            || constraintName === "maxHeight"
96
+                            || constraintName === "width"
97
+                            || constraintName === "height")) {
85 98
                     this.name = JitsiTrackErrors.UNSUPPORTED_RESOLUTION;
86
-                    this.message = TRACK_ERROR_TO_MESSAGE_MAP[
87
-                            JitsiTrackErrors.UNSUPPORTED_RESOLUTION] +
88
-                        getResolutionFromFailedConstraint(constraintName,
89
-                            options);
99
+                    this.message
100
+                        = TRACK_ERROR_TO_MESSAGE_MAP[this.name]
101
+                            + getResolutionFromFailedConstraint(
102
+                                    constraintName,
103
+                                    options);
90 104
                 } else {
91 105
                     this.name = JitsiTrackErrors.CONSTRAINT_FAILED;
92
-                    this.message = TRACK_ERROR_TO_MESSAGE_MAP[
93
-                            JitsiTrackErrors.CONSTRAINT_FAILED] +
94
-                        error.constraintName;
106
+                    this.message
107
+                        = TRACK_ERROR_TO_MESSAGE_MAP[this.name]
108
+                            + error.constraintName;
95 109
                 }
96 110
                 break;
97 111
             default:
98 112
                 this.name = JitsiTrackErrors.GENERAL;
99
-                this.message = error.message ||
100
-                    TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.GENERAL];
113
+                this.message
114
+                    = error.message || TRACK_ERROR_TO_MESSAGE_MAP[this.name];
101 115
                 break;
102
-        }
103
-    } else if (typeof error === "string") {
104
-        if (TRACK_ERROR_TO_MESSAGE_MAP[error]) {
105
-            this.name = error;
106
-            this.message = options || TRACK_ERROR_TO_MESSAGE_MAP[error];
116
+            }
117
+        } else if (typeof error === "string") {
118
+            if (TRACK_ERROR_TO_MESSAGE_MAP[error]) {
119
+                this.name = error;
120
+                this.message = options || TRACK_ERROR_TO_MESSAGE_MAP[error];
121
+            } else {
122
+                // this is some generic error that do not fit any of our
123
+                // pre-defined errors, so don't give it any specific name, just
124
+                // store message
125
+                this.message = error;
126
+            }
107 127
         } else {
108
-            // this is some generic error that do not fit any of our pre-defined
109
-            // errors, so don't give it any specific name, just store message
110
-            this.message = error;
128
+            throw new Error("Invalid arguments");
111 129
         }
112
-    } else {
113
-        throw new Error("Invalid arguments");
114
-    }
115 130
 
116
-    this.stack = error.stack || (new Error()).stack;
131
+        this.stack = error.stack || (new Error()).stack;
132
+    }
117 133
 }
118 134
 
119
-JitsiTrackError.prototype = Object.create(Error.prototype);
120
-JitsiTrackError.prototype.constructor = JitsiTrackError;
121
-
122 135
 /**
123 136
  * Gets failed resolution constraint from corresponding object.
124 137
  * @param {string} failedConstraintName
@@ -127,16 +140,15 @@ JitsiTrackError.prototype.constructor = JitsiTrackError;
127 140
  */
128 141
 function getResolutionFromFailedConstraint(failedConstraintName, constraints) {
129 142
     if (constraints && constraints.video && constraints.video.mandatory) {
130
-        if (failedConstraintName === "width") {
143
+        switch (failedConstraintName) {
144
+        case "width":
131 145
             return constraints.video.mandatory.minWidth;
132
-        } else if (failedConstraintName === "height") {
146
+        case "height":
133 147
             return constraints.video.mandatory.minHeight;
134
-        } else {
148
+        default:
135 149
             return constraints.video.mandatory[failedConstraintName] || "";
136 150
         }
137 151
     }
138 152
 
139 153
     return "";
140 154
 }
141
-
142
-module.exports = JitsiTrackError;

+ 67
- 60
JitsiTrackErrors.js View File

@@ -1,61 +1,68 @@
1 1
 /**
2
- * Enumeration with the errors for the JitsiTrack objects.
3
- * @type {{string: string}}
4
- */
5
-module.exports = {
6
-    /**
7
-     * An error which indicates that requested video resolution is not supported
8
-     * by a webcam.
9
-     */
10
-    UNSUPPORTED_RESOLUTION: "gum.unsupported_resolution",
11
-    /**
12
-     * An error which indicates that the jidesha extension for Firefox is
13
-     * needed to proceed with screen sharing, and that it is not installed.
14
-     */
15
-    FIREFOX_EXTENSION_NEEDED: "gum.firefox_extension_needed",
16
-    /**
17
-     * An error which indicates that the jidesha extension for Chrome is
18
-     * failed to install.
19
-     */
20
-    CHROME_EXTENSION_INSTALLATION_ERROR:
21
-        "gum.chrome_extension_installation_error",
22
-    /**
23
-     * An error which indicates that user canceled screen sharing window
24
-     * selection dialog in jidesha extension for Chrome.
25
-     */
26
-    CHROME_EXTENSION_USER_CANCELED:
27
-        "gum.chrome_extension_user_canceled",
28
-    /**
29
-     * Generic error for jidesha extension for Chrome.
30
-     */
31
-    CHROME_EXTENSION_GENERIC_ERROR:
32
-        "gum.chrome_extension_generic_error",
33
-    /**
34
-     * Generic getUserMedia error.
35
-     */
36
-    GENERAL: "gum.general",
37
-    /**
38
-     * An error which indicates that user denied permission to share requested
39
-     * device.
40
-     */
41
-    PERMISSION_DENIED: "gum.permission_denied",
42
-    /**
43
-     * An error which indicates that requested device was not found.
44
-     */
45
-    NOT_FOUND: "gum.not_found",
46
-    /**
47
-     * An error which indicates that some of requested constraints in
48
-     * getUserMedia call were not satisfied.
49
-     */
50
-    CONSTRAINT_FAILED: "gum.constraint_failed",
51
-    /**
52
-     * An error which indicates that track has been already disposed and cannot
53
-     * be longer used.
54
-     */
55
-    TRACK_IS_DISPOSED: "track.track_is_disposed",
56
-    /**
57
-     * An error which indicates that track is currently in progress of muting or
58
-     * unmuting itself.
59
-     */
60
-    TRACK_MUTE_UNMUTE_IN_PROGRESS: "track.mute_unmute_inprogress"
61
-};
2
+ * The errors for the JitsiTrack objects.
3
+ */
4
+
5
+/**
6
+ * Generic error for jidesha extension for Chrome.
7
+ */
8
+export const CHROME_EXTENSION_GENERIC_ERROR
9
+    = "gum.chrome_extension_generic_error";
10
+/**
11
+ * An error which indicates that the jidesha extension for Chrome is
12
+ * failed to install.
13
+ */
14
+export const CHROME_EXTENSION_INSTALLATION_ERROR
15
+    = "gum.chrome_extension_installation_error";
16
+/**
17
+ * An error which indicates that user canceled screen sharing window
18
+ * selection dialog in jidesha extension for Chrome.
19
+ */
20
+export const CHROME_EXTENSION_USER_CANCELED
21
+    ="gum.chrome_extension_user_canceled";
22
+/**
23
+ * An error which indicates that some of requested constraints in
24
+ * getUserMedia call were not satisfied.
25
+ */
26
+export const CONSTRAINT_FAILED = "gum.constraint_failed";
27
+/**
28
+ * An error which indicates that the jidesha extension for Firefox is
29
+ * needed to proceed with screen sharing, and that it is not installed.
30
+ */
31
+export const FIREFOX_EXTENSION_NEEDED = "gum.firefox_extension_needed";
32
+/**
33
+ * Generic getUserMedia error.
34
+ */
35
+export const GENERAL = "gum.general";
36
+/**
37
+ * An error which indicates that requested device was not found.
38
+ */
39
+export const NOT_FOUND = "gum.not_found";
40
+/**
41
+ * An error which indicates that user denied permission to share requested
42
+ * device.
43
+ */
44
+export const PERMISSION_DENIED = "gum.permission_denied";
45
+/**
46
+ * An error which indicates that track has been already disposed and cannot
47
+ * be longer used.
48
+ */
49
+export const TRACK_IS_DISPOSED = "track.track_is_disposed";
50
+/**
51
+ * An error which indicates that track is currently in progress of muting or
52
+ * unmuting itself.
53
+ */
54
+export const TRACK_MUTE_UNMUTE_IN_PROGRESS = "track.mute_unmute_inprogress";
55
+/**
56
+ * An error which indicates that track has no MediaStream associated.
57
+ */
58
+export const TRACK_NO_STREAM_FOUND = "track.no_stream_found";
59
+/**
60
+ * An error which indicates that requested video resolution is not supported
61
+ * by a webcam.
62
+ */
63
+export const UNSUPPORTED_RESOLUTION = "gum.unsupported_resolution";
64
+/**
65
+ * Indicates that the track is no receiving any data without reason(the
66
+ * stream was stopped, etc)
67
+ */
68
+export const NO_DATA_FROM_SOURCE = "track.no_data_from_source";

+ 25
- 23
JitsiTrackEvents.js View File

@@ -1,24 +1,26 @@
1
-var JitsiTrackEvents = {
2
-    /**
3
-     * A media track mute status was changed.
4
-     */
5
-    TRACK_MUTE_CHANGED: "track.trackMuteChanged",
6
-    /**
7
-     * Audio levels of a this track was changed.
8
-     */
9
-    TRACK_AUDIO_LEVEL_CHANGED: "track.audioLevelsChanged",
10
-    /**
11
-     * The media track was removed to the conference.
12
-     */
13
-    LOCAL_TRACK_STOPPED: "track.stopped",
14
-    /**
15
-     * The video type("camera" or "desktop") of the track was changed.
16
-     */
17
-    TRACK_VIDEOTYPE_CHANGED: "track.videoTypeChanged",
18
-    /**
19
-     * The audio output of the track was changed.
20
-     */
21
-    TRACK_AUDIO_OUTPUT_CHANGED: "track.audioOutputChanged"
22
-};
1
+/**
2
+ * The media track was removed to the conference.
3
+ */
4
+export const LOCAL_TRACK_STOPPED = "track.stopped";
5
+/**
6
+ * Audio levels of a this track was changed.
7
+ */
8
+export const TRACK_AUDIO_LEVEL_CHANGED = "track.audioLevelsChanged";
23 9
 
24
-module.exports = JitsiTrackEvents;
10
+/**
11
+ * The audio output of the track was changed.
12
+ */
13
+export const TRACK_AUDIO_OUTPUT_CHANGED = "track.audioOutputChanged";
14
+/**
15
+ * A media track mute status was changed.
16
+ */
17
+export const TRACK_MUTE_CHANGED = "track.trackMuteChanged";
18
+/**
19
+ * The video type("camera" or "desktop") of the track was changed.
20
+ */
21
+export const TRACK_VIDEOTYPE_CHANGED = "track.videoTypeChanged";
22
+/**
23
+ * Indicates that the track is no receiving any data without reason(the
24
+ * stream was stopped, etc)
25
+ */
26
+export const NO_DATA_FROM_SOURCE = "track.no_data_from_source";

+ 15
- 9
connection_optimization/external_connect.js View File

@@ -20,8 +20,10 @@
20 20
  * callback is going to receive one parameter which is going to be JS error
21 21
  * object with a reason for failure in it.
22 22
  */
23
-function createConnectionExternally(webserviceUrl, success_callback,
24
-    error_callback) {
23
+function createConnectionExternally( // eslint-disable-line no-unused-vars
24
+        webserviceUrl,
25
+        success_callback,
26
+        error_callback) {
25 27
     if (!window.XMLHttpRequest) {
26 28
         error_callback(new Error("XMLHttpRequest is not supported!"));
27 29
         return;
@@ -40,13 +42,13 @@ function createConnectionExternally(webserviceUrl, success_callback,
40 42
                 try {
41 43
                     var data = JSON.parse(xhttp.responseText);
42 44
 
45
+                    var proxyRegion = xhttp.getResponseHeader('X-Proxy-Region');
46
+                    var jitsiRegion = xhttp.getResponseHeader('X-Jitsi-Region');
43 47
                     window.jitsiRegionInfo = {
44
-                        "ProxyRegion" :
45
-                            xhttp.getResponseHeader('X-Proxy-Region'),
46
-                        "Region" :
47
-                            xhttp.getResponseHeader('X-Jitsi-Region'),
48
-                        "Shard" :
49
-                            xhttp.getResponseHeader('X-Jitsi-Shard')
48
+                        "ProxyRegion" : proxyRegion,
49
+                        "Region" : jitsiRegion,
50
+                        "Shard" : xhttp.getResponseHeader('X-Jitsi-Shard'),
51
+                        "CrossRegion": proxyRegion !== jitsiRegion ? 1 : 0
50 52
                     };
51 53
 
52 54
                     success_callback(data);
@@ -60,9 +62,13 @@ function createConnectionExternally(webserviceUrl, success_callback,
60 62
         }
61 63
     };
62 64
 
65
+    xhttp.open("GET", webserviceUrl, true);
66
+
67
+    // Fixes external connect for IE
68
+    // The timeout property may be set only after calling the open() method
69
+    // and before calling the send() method.
63 70
     xhttp.timeout = 3000;
64 71
 
65
-    xhttp.open("GET", webserviceUrl, true);
66 72
     window.connectionTimes = {};
67 73
     var now = window.connectionTimes["external_connect.sending"] =
68 74
         window.performance.now();

+ 33
- 5
doc/API.md View File

@@ -50,6 +50,10 @@ The ```options``` parameter is JS object with the following properties:
50 50
     9. disableAudioLevels - boolean property. Enables/disables audio levels.
51 51
     10. disableSimulcast - boolean property. Enables/disables simulcast.
52 52
     11. enableWindowOnErrorHandler - boolean property (default false). Enables/disables attaching global onerror handler (window.onerror).
53
+    12. disableThirdPartyRequests - if true - callstats will be disabled and the callstats API won't be included.
54
+    13. analyticsScriptUrl - (optional) custom url to search for the analytics lib, if missing js file will be expected to be next to the library file (the location it is sourced from)
55
+    14. callStatsCustomScriptUrl - (optional) custom url to access callstats client script 
56
+    15. callStatsConfIDNamespace - (optional) a namespace to prepend the callstats conference ID with. Defaults to the window.location.hostname
53 57
 
54 58
 * ```JitsiMeetJS.JitsiConnection``` - the ```JitsiConnection``` constructor. You can use that to create new server connection.
55 59
 
@@ -118,6 +122,8 @@ JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
118 122
         - AVAILABLE_DEVICES_CHANGED - notifies that available participant devices changed (camera or microphone was added or removed) (parameters - id(string), devices(JS object with 2 properties - audio(boolean), video(boolean)))
119 123
         - CONNECTION_STATS - New local connection statistics are received. (parameters - stats(object))
120 124
         - AUTH_STATUS_CHANGED - notifies that authentication is enabled or disabled, or local user authenticated (logged in). (parameters - isAuthEnabled(boolean), authIdentity(string))
125
+        - ENDPOINT_MESSAGE_RECEIVED - notifies that a new message
126
+        from another participant is received on a data channel.
121 127
 
122 128
     2. connection
123 129
         - CONNECTION_FAILED - indicates that the server connection failed.
@@ -165,15 +171,16 @@ JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
165 171
         - NOT_FOUND - getUserMedia-related error, indicates that requested device was not found.
166 172
         - CONSTRAINT_FAILED - getUserMedia-related error, indicates that some of requested constraints in getUserMedia call were not satisfied.
167 173
         - TRACK_IS_DISPOSED - an error which indicates that track has been already disposed and cannot be longer used.
174
+        - TRACK_NO_STREAM_FOUND - an error which indicates that track has no MediaStream associated.
168 175
         - TRACK_MUTE_UNMUTE_IN_PROGRESS - an error which indicates that track is currently in progress of muting or unmuting itself.
169 176
         - CHROME_EXTENSION_GENERIC_ERROR - generic error for jidesha extension for Chrome.
170 177
         - CHROME_EXTENSION_USER_CANCELED - an error which indicates that user canceled screen sharing window selection dialog in jidesha extension for Chrome.
171 178
         - CHROME_EXTENSION_INSTALLATION_ERROR - an error which indicates that the jidesha extension for Chrome is failed to install.
172 179
         - FIREFOX_EXTENSION_NEEDED - An error which indicates that the jidesha extension for Firefox is needed to proceed with screen sharing, and that it is not installed.
173
-        
180
+
174 181
 * ```JitsiMeetJS.errorTypes``` - constructors for Error instances that can be produced by library. Are useful for checks like ```error instanceof JitsiMeetJS.errorTypes.JitsiTrackError```. Following Errors are available:
175 182
     1. ```JitsiTrackError``` - Error that happened to a JitsiTrack.
176
-        
183
+
177 184
 * ```JitsiMeetJS.logLevels``` - object with the log levels:
178 185
     1. TRACE
179 186
     2. DEBUG
@@ -198,6 +205,7 @@ This objects represents the server connection. You can create new ```JitsiConnec
198 205
             - muc
199 206
             - anonymousdomain
200 207
         3. useStunTurn -
208
+        4. enableLipSync - (optional) boolean property which enables the lipsync feature. Currently works only in Chrome and is enabled by default.
201 209
 
202 210
 2. connect(options) - establish server connection
203 211
     - options - JS Object with ```id``` and ```password``` properties.
@@ -212,8 +220,7 @@ This objects represents the server connection. You can create new ```JitsiConnec
212 220
         3. jirecon
213 221
         4. callStatsID - callstats credentials
214 222
         5. callStatsSecret - callstats credentials
215
-        6. disableThirdPartyRequests - if true - callstats will be disabled and
216
-        the callstats API won't be included.
223
+        6. enableTalkWhileMuted - boolean property. Enables/disables talk while muted detection, by default the value is false/disabled.
217 224
         **NOTE: if 4 and 5 are set the library is going to send events to callstats. Otherwise the callstats integration will be disabled.**
218 225
 
219 226
 5. addEventListener(event, listener) - Subscribes the passed listener to the event.
@@ -341,6 +348,27 @@ The object represents a conference. We have the following methods to control the
341 348
 
342 349
     Note: available only for moderator
343 350
 
351
+31. sendEndpointMessage(to, payload) - Sends message via the data channels.
352
+    - to - the id of the endpoint that should receive the message. If "" the message will be sent to all participants.
353
+    - payload - JSON object - the payload of the message.
354
+
355
+Throws NetworkError or InvalidStateError or Error if the operation fails.
356
+
357
+32. broadcastEndpointMessage(payload) - Sends broadcast message via the datachannels.
358
+    - payload - JSON object - the payload of the message.
359
+
360
+Throws NetworkError or InvalidStateError or Error if the operation fails.
361
+
362
+33. selectParticipant(participantId) - Elects the participant with the given id to be the selected participant in order to receive higher video quality (if simulcast is enabled).
363
+    - participantId - the identifier of the participant
364
+
365
+Throws NetworkError or InvalidStateError or Error if the operation fails.
366
+
367
+34. pinParticipant(participantId) - Elects the participant with the given id to be the pinned participant in order to always receive video for this participant (even when last n is enabled).
368
+    - participantId - the identifier of the participant
369
+
370
+Throws NetworkError or InvalidStateError or Error if the operation fails.
371
+
344 372
 JitsiTrack
345 373
 ======
346 374
 The object represents single track - video or audio. They can be remote tracks ( from the other participants in the call) or local tracks (from the devices of the local participant).
@@ -381,7 +409,7 @@ We have the following methods for controling the tracks:
381 409
 
382 410
 JitsiTrackError
383 411
 ======
384
-The object represents error that happened to a JitsiTrack. Is inherited from JavaScript base ```Error``` object, 
412
+The object represents error that happened to a JitsiTrack. Is inherited from JavaScript base ```Error``` object,
385 413
 so ```"name"```, ```"message"``` and ```"stack"``` properties are available. For GUM-related errors,
386 414
 exposes additional ```"gum"``` property, which is an object with following properties:
387 415
  - error - original GUM error

+ 14
- 13
doc/example/example.js View File

@@ -1,3 +1,5 @@
1
+/* global $, JitsiMeetJS */
2
+
1 3
 var options = {
2 4
     hosts: {
3 5
         domain: 'jitsi-meet.example.com',
@@ -5,12 +7,11 @@ var options = {
5 7
     },
6 8
     bosh: '//jitsi-meet.example.com/http-bind', // FIXME: use xep-0156 for that
7 9
     clientNode: 'http://jitsi.org/jitsimeet', // The name of client node advertised in XEP-0115 'c' stanza
8
-}
10
+};
9 11
 
10 12
 var confOptions = {
11 13
     openSctp: true
12
-}
13
-
14
+};
14 15
 
15 16
 var isJoined = false;
16 17
 
@@ -103,7 +104,7 @@ function onUserLeft(id) {
103 104
         return;
104 105
     var tracks = remoteTracks[id];
105 106
     for(var i = 0; i< tracks.length; i++)
106
-        tracks[i].detach($("#" + id + tracks[i].getType()))
107
+        tracks[i].detach($("#" + id + tracks[i].getType()));
107 108
 }
108 109
 
109 110
 /**
@@ -139,12 +140,14 @@ function onConnectionSuccess(){
139 140
             room.getPhonePin());
140 141
     });
141 142
     room.join();
142
-};
143
+}
143 144
 
144 145
 /**
145 146
  * This function is called when the connection fail.
146 147
  */
147
-function onConnectionFailed(){console.error("Connection Failed!")};
148
+function onConnectionFailed() {
149
+    console.error("Connection Failed!");
150
+}
148 151
 
149 152
 /**
150 153
  * This function is called when the connection fail.
@@ -169,8 +172,9 @@ function unload() {
169 172
     room.leave();
170 173
     connection.disconnect();
171 174
 }
175
+
172 176
 var isVideo = true;
173
-function switchVideo() {
177
+function switchVideo() { // eslint-disable-line no-unused-vars
174 178
     isVideo = !isVideo;
175 179
     if(localTracks[1]) {
176 180
         localTracks[1].dispose();
@@ -194,15 +198,13 @@ function switchVideo() {
194 198
         });
195 199
 }
196 200
 
197
-function changeAudioOutput(selected) {
201
+function changeAudioOutput(selected) { // eslint-disable-line no-unused-vars
198 202
     JitsiMeetJS.mediaDevices.setAudioOutputDevice(selected.value);
199 203
 }
200 204
 
201 205
 $(window).bind('beforeunload', unload);
202 206
 $(window).bind('unload', unload);
203 207
 
204
-
205
-
206 208
 // JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
207 209
 var initOptions = {
208 210
     disableAudioLevels: true,
@@ -229,7 +231,7 @@ var initOptions = {
229 231
     desktopSharingFirefoxMaxVersionExtRequired: -1,
230 232
     // The URL to the Firefox extension for desktop sharing.
231 233
     desktopSharingFirefoxExtensionURL: null
232
-}
234
+};
233 235
 JitsiMeetJS.init(initOptions).then(function(){
234 236
     connection = new JitsiMeetJS.JitsiConnection(null, null, options);
235 237
 
@@ -248,7 +250,6 @@ JitsiMeetJS.init(initOptions).then(function(){
248 250
     console.log(error);
249 251
 });
250 252
 
251
-
252 253
 if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
253 254
     JitsiMeetJS.mediaDevices.enumerateDevices(function(devices) {
254 255
         var audioOutputDevices = devices.filter(function(d) { return d.kind === 'audiooutput'; });
@@ -262,7 +263,7 @@ if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
262 263
 
263 264
             $('#audioOutputSelectWrapper').show();
264 265
         }
265
-    })
266
+    });
266 267
 }
267 268
 
268 269
 var connection = null;

+ 79
- 43
modules/RTC/DataChannels.js View File

@@ -1,5 +1,3 @@
1
-/* global config, APP, Strophe */
2
-
3 1
 // cache datachannels to avoid garbage collection
4 2
 // https://code.google.com/p/chromium/issues/detail?id=405545
5 3
 
@@ -7,7 +5,6 @@ var logger = require("jitsi-meet-logger").getLogger(__filename);
7 5
 var RTCEvents = require("../../service/RTC/RTCEvents");
8 6
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
9 7
 
10
-
11 8
 /**
12 9
  * Binds "ondatachannel" event listener to given PeerConnection instance.
13 10
  * @param peerConnection WebRTC peer connection instance.
@@ -40,8 +37,7 @@ function DataChannels(peerConnection, emitter) {
40 37
      var msgData = event.data;
41 38
      logger.info("Got My Data Channel Message:", msgData, dataChannel);
42 39
      };*/
43
-};
44
-
40
+}
45 41
 
46 42
 /**
47 43
  * Callback triggered by PeerConnection when new data channel is opened
@@ -51,7 +47,6 @@ function DataChannels(peerConnection, emitter) {
51 47
 DataChannels.prototype.onDataChannel = function (event) {
52 48
     var dataChannel = event.channel;
53 49
     var self = this;
54
-    var selectedEndpoint = null;
55 50
 
56 51
     dataChannel.onopen = function () {
57 52
         logger.info("Data channel opened by the Videobridge!", dataChannel);
@@ -63,18 +58,14 @@ DataChannels.prototype.onDataChannel = function (event) {
63 58
         //dataChannel.send(new ArrayBuffer(12));
64 59
 
65 60
         self.eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
66
-
67
-        // when the data channel becomes available, tell the bridge about video
68
-        // selections so that it can do adaptive simulcast,
69
-        // we want the notification to trigger even if userJid is undefined,
70
-        // or null.
71
-        // XXX why do we not do the same for pinned endpoints?
72
-        self.sendSelectedEndpointMessage(self.selectedEndpoint);
73 61
     };
74 62
 
75 63
     dataChannel.onerror = function (error) {
76
-        var e = new Error("Data Channel Error:" + error);
77
-        GlobalOnErrorHandler.callErrorHandler(e);
64
+        // FIXME: this one seems to be generated a bit too often right now
65
+        // so we are temporarily commenting it before we have more clarity
66
+        // on which of the errors we absolutely need to report
67
+        //GlobalOnErrorHandler.callErrorHandler(
68
+        //        new Error("Data Channel Error:" + error));
78 69
         logger.error("Data Channel Error:", error, dataChannel);
79 70
     };
80 71
 
@@ -104,7 +95,8 @@ DataChannels.prototype.onDataChannel = function (event) {
104 95
                 logger.info(
105 96
                     "Data channel new dominant speaker event: ",
106 97
                     dominantSpeakerEndpoint);
107
-                self.eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
98
+                self.eventEmitter.emit(RTCEvents.DOMINANT_SPEAKER_CHANGED,
99
+                  dominantSpeakerEndpoint);
108 100
             }
109 101
             else if ("InLastNChangeEvent" === colibriClass) {
110 102
                 var oldValue = obj.oldValue;
@@ -143,6 +135,18 @@ DataChannels.prototype.onDataChannel = function (event) {
143 135
                     lastNEndpoints, endpointsEnteringLastN, obj);
144 136
                 self.eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
145 137
                     lastNEndpoints, endpointsEnteringLastN, obj);
138
+            } else if("EndpointMessage" === colibriClass) {
139
+                self.eventEmitter.emit(
140
+                    RTCEvents.ENDPOINT_MESSAGE_RECEIVED, obj.from,
141
+                    obj.msgPayload);
142
+            }
143
+            else if ("EndpointConnectivityStatusChangeEvent" === colibriClass) {
144
+                var endpoint = obj.endpoint;
145
+                var isActive = obj.active === "true";
146
+                logger.info("Endpoint connection status changed: " + endpoint
147
+                           + " active ? " + isActive);
148
+                self.eventEmitter.emit(RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
149
+                    endpoint, isActive);
146 150
             }
147 151
             else {
148 152
                 logger.debug("Data channel JSON-formatted message: ", obj);
@@ -176,26 +180,36 @@ DataChannels.prototype.closeAllChannels = function () {
176 180
 
177 181
 /**
178 182
  * Sends a "selected endpoint changed" message via the data channel.
183
+ * @param endpointId {string} the id of the selected endpoint
184
+ * @throws NetworkError or InvalidStateError from RTCDataChannel#send (@see
185
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send})
186
+ * or Error with "No opened data channels found!" message.
179 187
  */
180 188
 DataChannels.prototype.sendSelectedEndpointMessage = function (endpointId) {
181
-    this.selectedEndpoint = endpointId;
182 189
     this._onXXXEndpointChanged("selected", endpointId);
183 190
 };
184 191
 
185 192
 /**
186 193
  * Sends a "pinned endpoint changed" message via the data channel.
194
+ * @param endpointId {string} the id of the pinned endpoint
195
+ * @throws NetworkError or InvalidStateError from RTCDataChannel#send (@see
196
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send})
197
+ * or Error with "No opened data channels found!" message.
187 198
  */
188 199
 DataChannels.prototype.sendPinnedEndpointMessage = function (endpointId) {
189
-    this._onXXXEndpointChanged("pinnned", endpointId);
200
+    this._onXXXEndpointChanged("pinned", endpointId);
190 201
 };
191 202
 
192 203
 /**
193 204
  * Notifies Videobridge about a change in the value of a specific
194
- * endpoint-related property such as selected endpoint and pinnned endpoint.
205
+ * endpoint-related property such as selected endpoint and pinned endpoint.
195 206
  *
196 207
  * @param xxx the name of the endpoint-related property whose value changed
197 208
  * @param userResource the new value of the endpoint-related property after the
198 209
  * change
210
+ * @throws NetworkError or InvalidStateError from RTCDataChannel#send (@see
211
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send})
212
+ * or Error with "No opened data channels found!" message.
199 213
  */
200 214
 DataChannels.prototype._onXXXEndpointChanged = function (xxx, userResource) {
201 215
     // Derive the correct words from xxx such as selected and Selected, pinned
@@ -204,34 +218,21 @@ DataChannels.prototype._onXXXEndpointChanged = function (xxx, userResource) {
204 218
     var tail = xxx.substring(1);
205 219
     var lower = head.toLowerCase() + tail;
206 220
     var upper = head.toUpperCase() + tail;
221
+    logger.log(
222
+            'sending ' + lower
223
+                + ' endpoint changed notification to the bridge: ',
224
+            userResource);
207 225
 
208
-    // Notify Videobridge about the specified endpoint change.
209
-    logger.log(lower + ' endpoint changed: ', userResource);
210
-    this._some(function (dataChannel) {
211
-        if (dataChannel.readyState == 'open') {
212
-            logger.log(
213
-                    'sending ' + lower
214
-                        + ' endpoint changed notification to the bridge: ',
215
-                    userResource);
226
+    var jsonObject = {};
216 227
 
217
-            var jsonObject = {};
228
+    jsonObject.colibriClass = (upper + 'EndpointChangedEvent');
229
+    jsonObject[lower + "Endpoint"]
230
+        = (userResource ? userResource : null);
218 231
 
219
-            jsonObject.colibriClass = (upper + 'EndpointChangedEvent');
220
-            jsonObject[lower + "Endpoint"]
221
-                = (userResource ? userResource : null);
222
-            try {
223
-                dataChannel.send(JSON.stringify(jsonObject));
224
-            } catch (e) {
225
-                // FIXME: Maybe we should check if the conference is left
226
-                // before calling _onXXXEndpointChanged method.
227
-                // FIXME: We should check if we are disposing correctly the
228
-                // data channels.
229
-                logger.warn(e);
230
-            }
232
+    this.send(jsonObject);
231 233
 
232
-            return true;
233
-        }
234
-    });
234
+    // Notify Videobridge about the specified endpoint change.
235
+    logger.log(lower + ' endpoint changed: ', userResource);
235 236
 };
236 237
 
237 238
 DataChannels.prototype._some = function (callback, thisArg) {
@@ -247,4 +248,39 @@ DataChannels.prototype._some = function (callback, thisArg) {
247 248
     }
248 249
 };
249 250
 
251
+/**
252
+ * Sends passed object via the first found open datachannel
253
+ * @param jsonObject {object} the object that will be sent
254
+ * @throws NetworkError or InvalidStateError from RTCDataChannel#send (@see
255
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send})
256
+ * or Error with "No opened data channels found!" message.
257
+ */
258
+DataChannels.prototype.send = function (jsonObject) {
259
+    if(!this._some(function (dataChannel) {
260
+        if (dataChannel.readyState == 'open') {
261
+                dataChannel.send(JSON.stringify(jsonObject));
262
+            return true;
263
+        }
264
+    })) {
265
+        throw new Error("No opened data channels found!");
266
+    }
267
+};
268
+
269
+/**
270
+ * Sends message via the datachannels.
271
+ * @param to {string} the id of the endpoint that should receive the message.
272
+ * If "" the message will be sent to all participants.
273
+ * @param payload {object} the payload of the message.
274
+ * @throws NetworkError or InvalidStateError from RTCDataChannel#send (@see
275
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send})
276
+ * or Error with "No opened data channels found!" message.
277
+ */
278
+DataChannels.prototype.sendDataChannelMessage = function (to, payload) {
279
+    this.send({
280
+        colibriClass: "EndpointMessage",
281
+        to: to,
282
+        msgPayload: payload
283
+    });
284
+};
285
+
250 286
 module.exports = DataChannels;

+ 401
- 148
modules/RTC/JitsiLocalTrack.js View File

@@ -1,12 +1,15 @@
1 1
 /* global __filename, Promise */
2
-var logger = require("jitsi-meet-logger").getLogger(__filename);
2
+var CameraFacingMode = require('../../service/RTC/CameraFacingMode');
3 3
 var JitsiTrack = require("./JitsiTrack");
4
+import JitsiTrackError from "../../JitsiTrackError";
5
+import * as JitsiTrackErrors from "../../JitsiTrackErrors";
6
+import * as JitsiTrackEvents from "../../JitsiTrackEvents";
7
+var logger = require("jitsi-meet-logger").getLogger(__filename);
8
+var MediaType = require('../../service/RTC/MediaType');
4 9
 var RTCBrowserType = require("./RTCBrowserType");
5
-var JitsiTrackEvents = require('../../JitsiTrackEvents');
6
-var JitsiTrackErrors = require("../../JitsiTrackErrors");
7
-var JitsiTrackError = require("../../JitsiTrackError");
8 10
 var RTCEvents = require("../../service/RTC/RTCEvents");
9 11
 var RTCUtils = require("./RTCUtils");
12
+var Statistics = require("../statistics/statistics");
10 13
 var VideoType = require('../../service/RTC/VideoType');
11 14
 
12 15
 /**
@@ -18,10 +21,11 @@ var VideoType = require('../../service/RTC/VideoType');
18 21
  * @param videoType the VideoType of the JitsiRemoteTrack
19 22
  * @param resolution the video resoultion if it's a video track
20 23
  * @param deviceId the ID of the local device for this track
24
+ * @param facingMode the camera facing mode used in getUserMedia call
21 25
  * @constructor
22 26
  */
23 27
 function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
24
-                         deviceId) {
28
+                         deviceId, facingMode) {
25 29
     var self = this;
26 30
 
27 31
     JitsiTrack.call(this,
@@ -35,26 +39,60 @@ function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
35 39
         mediaType, videoType, null /* ssrc */);
36 40
     this.dontFireRemoveEvent = false;
37 41
     this.resolution = resolution;
42
+
43
+    // FIXME: currently firefox is ignoring our constraints about resolutions
44
+    // so we do not store it, to avoid wrong reporting of local track resolution
45
+    if (RTCBrowserType.isFirefox())
46
+        this.resolution = null;
47
+
38 48
     this.deviceId = deviceId;
39 49
     this.startMuted = false;
40
-    this.disposed = false;
41
-    //FIXME: This dependacy is not necessary.
42
-    this.conference = null;
43 50
     this.initialMSID = this.getMSID();
44 51
     this.inMuteOrUnmuteProgress = false;
45 52
 
53
+    /**
54
+     * The facing mode of the camera from which this JitsiLocalTrack instance
55
+     * was obtained.
56
+     */
57
+    this._facingMode = facingMode;
58
+
46 59
     // Currently there is no way to know the MediaStreamTrack ended due to to
47 60
     // device disconnect in Firefox through e.g. "readyState" property. Instead
48 61
     // we will compare current track's label with device labels from
49 62
     // enumerateDevices() list.
50 63
     this._trackEnded = false;
51 64
 
65
+    /**
66
+     * The value of bytes sent received from the statistics module.
67
+     */
68
+    this._bytesSent = null;
69
+
70
+    /**
71
+     * Used only for detection of audio problems. We want to check only once
72
+     * whether the track is sending bytes ot not. This flag is set to false
73
+     * after the check.
74
+     */
75
+    this._testByteSent = true;
76
+
52 77
     // Currently there is no way to determine with what device track was
53 78
     // created (until getConstraints() support), however we can associate tracks
54 79
     // with real devices obtained from enumerateDevices() call as soon as it's
55 80
     // called.
56 81
     this._realDeviceId = this.deviceId === '' ? undefined : this.deviceId;
57 82
 
83
+    /**
84
+     * Indicates that we have called RTCUtils.stopMediaStream for the
85
+     * MediaStream related to this JitsiTrack object.
86
+     */
87
+    this.stopStreamInProgress = false;
88
+
89
+    /**
90
+     * On mute event we are waiting for 3s to check if the stream is going to
91
+     * be still muted before firing the event for camera issue detected
92
+     * (NO_DATA_FROM_SOURCE).
93
+     */
94
+    this._noDataFromSourceTimeout = null;
95
+
58 96
     this._onDeviceListChanged = function (devices) {
59 97
         self._setRealDeviceIdFromDeviceList(devices);
60 98
 
@@ -83,6 +121,8 @@ function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
83 121
 
84 122
     RTCUtils.addListener(RTCEvents.DEVICE_LIST_CHANGED,
85 123
         this._onDeviceListChanged);
124
+
125
+    this._initNoDataFromSourceHandlers();
86 126
 }
87 127
 
88 128
 JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype);
@@ -96,6 +136,68 @@ JitsiLocalTrack.prototype.isEnded = function () {
96 136
     return  this.getTrack().readyState === 'ended' || this._trackEnded;
97 137
 };
98 138
 
139
+/**
140
+ * Sets handlers to the MediaStreamTrack object that will detect camera issues.
141
+ */
142
+JitsiLocalTrack.prototype._initNoDataFromSourceHandlers = function () {
143
+    if(this.isVideoTrack() && this.videoType === VideoType.CAMERA) {
144
+        let _onNoDataFromSourceError
145
+            = this._onNoDataFromSourceError.bind(this);
146
+        this._setHandler("track_mute", () => {
147
+            if(this._checkForCameraIssues()) {
148
+                let now = window.performance.now();
149
+                this._noDataFromSourceTimeout
150
+                    = setTimeout(_onNoDataFromSourceError, 3000);
151
+                this._setHandler("track_unmute", () => {
152
+                    this._clearNoDataFromSourceMuteResources();
153
+                    Statistics.sendEventToAll(
154
+                        this.getType() + ".track_unmute",
155
+                        {value: window.performance.now() - now});
156
+                });
157
+            }
158
+        });
159
+        this._setHandler("track_ended", _onNoDataFromSourceError);
160
+    }
161
+};
162
+
163
+/**
164
+ * Clears all timeouts and handlers set on MediaStreamTrack mute event.
165
+ * FIXME: Change the name of the method with better one.
166
+ */
167
+JitsiLocalTrack.prototype._clearNoDataFromSourceMuteResources = function () {
168
+    if(this._noDataFromSourceTimeout) {
169
+        clearTimeout(this._noDataFromSourceTimeout);
170
+        this._noDataFromSourceTimeout = null;
171
+    }
172
+    this._setHandler("track_unmute", undefined);
173
+};
174
+
175
+/**
176
+ * Called when potential camera issue is detected. Clears the handlers and
177
+ * timeouts set on MediaStreamTrack muted event. Verifies that the camera
178
+ * issue persists and fires NO_DATA_FROM_SOURCE event.
179
+ */
180
+JitsiLocalTrack.prototype._onNoDataFromSourceError = function () {
181
+    this._clearNoDataFromSourceMuteResources();
182
+    if(this._checkForCameraIssues())
183
+        this._fireNoDataFromSourceEvent();
184
+};
185
+
186
+/**
187
+ * Fires JitsiTrackEvents.NO_DATA_FROM_SOURCE and logs it to analytics and
188
+ * callstats.
189
+ */
190
+JitsiLocalTrack.prototype._fireNoDataFromSourceEvent = function () {
191
+    this.eventEmitter.emit(JitsiTrackEvents.NO_DATA_FROM_SOURCE);
192
+    let eventName = this.getType() + ".no_data_from_source";
193
+    Statistics.analytics.sendEvent(eventName);
194
+    let log = {name: eventName};
195
+    if (this.isAudioTrack()) {
196
+        log.isReceivingData = this._isReceivingData();
197
+    }
198
+    Statistics.sendLog(JSON.stringify(log));
199
+};
200
+
99 201
 /**
100 202
  * Sets real device ID by comparing track information with device information.
101 203
  * This is temporary solution until getConstraints() method will be implemented
@@ -134,162 +236,211 @@ JitsiLocalTrack.prototype.unmute = function () {
134 236
 
135 237
 /**
136 238
  * Creates Promise for mute/unmute operation.
137
- * @param track the track that will be muted/unmuted
138
- * @param mute whether to mute or unmute the track
239
+ *
240
+ * @param {JitsiLocalTrack} track - The track that will be muted/unmuted.
241
+ * @param {boolean} mute - Whether to mute or unmute the track.
242
+ * @returns {Promise}
139 243
  */
140
-function createMuteUnmutePromise(track, mute)
141
-{
142
-    return new Promise(function (resolve, reject) {
143
-
144
-        if(this.inMuteOrUnmuteProgress) {
145
-            reject(new JitsiTrackError(
146
-                JitsiTrackErrors.TRACK_MUTE_UNMUTE_IN_PROGRESS));
147
-            return;
148
-        }
149
-        this.inMuteOrUnmuteProgress = true;
150
-
151
-        this._setMute(mute,
152
-            function(){
153
-                this.inMuteOrUnmuteProgress = false;
154
-                resolve();
155
-            }.bind(this),
156
-            function(status){
157
-                this.inMuteOrUnmuteProgress = false;
158
-                reject(status);
159
-            }.bind(this));
160
-    }.bind(track));
244
+function createMuteUnmutePromise(track, mute) {
245
+    if (track.inMuteOrUnmuteProgress) {
246
+        return Promise.reject(
247
+            new JitsiTrackError(JitsiTrackErrors.TRACK_MUTE_UNMUTE_IN_PROGRESS)
248
+        );
249
+    }
250
+
251
+    track.inMuteOrUnmuteProgress = true;
252
+
253
+    return track._setMute(mute)
254
+        .then(function() {
255
+            track.inMuteOrUnmuteProgress = false;
256
+        })
257
+        .catch(function(status) {
258
+            track.inMuteOrUnmuteProgress = false;
259
+            throw status;
260
+        });
161 261
 }
162 262
 
163 263
 /**
164 264
  * Mutes / unmutes the track.
165
- * @param mute {boolean} if true the track will be muted. Otherwise the track
265
+ *
266
+ * @param {boolean} mute - If true the track will be muted. Otherwise the track
166 267
  * will be unmuted.
268
+ * @private
269
+ * @returns {Promise}
167 270
  */
168
-JitsiLocalTrack.prototype._setMute = function (mute, resolve, reject) {
271
+JitsiLocalTrack.prototype._setMute = function (mute) {
169 272
     if (this.isMuted() === mute) {
170
-        resolve();
171
-        return;
273
+        return Promise.resolve();
172 274
     }
173
-    if(!this.rtc) {
174
-        this.startMuted = mute;
175
-        resolve();
176
-        return;
177
-    }
178
-    var isAudio = this.isAudioTrack();
179
-    this.dontFireRemoveEvent = false;
180 275
 
181
-    var setStreamToNull = false;
182
-    // the callback that will notify that operation had finished
183
-    var callbackFunction = function() {
276
+    var promise = Promise.resolve();
277
+    var self = this;
184 278
 
185
-        if(setStreamToNull)
186
-            this.stream = null;
187
-        this.eventEmitter.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED);
279
+    // Local track can be used out of conference, so we need to handle that
280
+    // case and mark that track should start muted or not when added to
281
+    // conference.
282
+    if(!this.conference || !this.conference.room) {
283
+        this.startMuted = mute;
284
+    }
188 285
 
189
-        resolve();
190
-    }.bind(this);
286
+    this.dontFireRemoveEvent = false;
191 287
 
192
-    if ((window.location.protocol != "https:") ||
193
-        (isAudio) || this.videoType === VideoType.DESKTOP ||
194
-        // FIXME FF does not support 'removeStream' method used to mute
288
+    // FIXME FF does not support 'removeStream' method used to mute
289
+    if (window.location.protocol !== "https:" ||
290
+        this.isAudioTrack() ||
291
+        this.videoType === VideoType.DESKTOP ||
195 292
         RTCBrowserType.isFirefox()) {
196
-
197
-        if (this.track)
293
+        if(this.track)
198 294
             this.track.enabled = !mute;
199
-        if(isAudio)
200
-            this.rtc.room.setAudioMute(mute, callbackFunction);
201
-        else
202
-            this.rtc.room.setVideoMute(mute, callbackFunction);
203 295
     } else {
204
-        if (mute) {
296
+        if(mute) {
205 297
             this.dontFireRemoveEvent = true;
206
-            this.rtc.room.removeStream(this.stream, function () {
207
-                    RTCUtils.stopMediaStream(this.stream);
208
-                    setStreamToNull = true;
209
-                    if(isAudio)
210
-                        this.rtc.room.setAudioMute(mute, callbackFunction);
211
-                    else
212
-                        this.rtc.room.setVideoMute(mute, callbackFunction);
213
-                    //FIXME: Maybe here we should set the SRC for the containers to something
214
-                }.bind(this),
215
-                function (error) {
216
-                    reject(error);
217
-                }, {mtype: this.type, type: "mute", ssrc: this.ssrc});
218
-
298
+            promise = new Promise( (resolve, reject) => {
299
+                this._removeStreamFromConferenceAsMute(() => {
300
+                    //FIXME: Maybe here we should set the SRC for the containers
301
+                    // to something
302
+                    this._stopMediaStream();
303
+                    this._setStream(null);
304
+                    resolve();
305
+                }, (err) => {
306
+                    reject(err);
307
+                });
308
+            });
219 309
         } else {
220
-            var self = this;
221
-            // FIXME why are we doing all this audio type checks and
222
-            // convoluted scenarios if we're going this way only
223
-            // for VIDEO media and CAMERA type of video ?
310
+            // This path is only for camera.
224 311
             var streamOptions = {
225
-                devices: (isAudio ? ["audio"] : ["video"]),
226
-                resolution: self.resolution
312
+                cameraDeviceId: this.getDeviceId(),
313
+                devices: [ MediaType.VIDEO ],
314
+                facingMode: this.getCameraFacingMode()
227 315
             };
228
-            if (isAudio) {
229
-                streamOptions['micDeviceId'] = self.getDeviceId();
230
-            } else if(self.videoType === VideoType.CAMERA) {
231
-                streamOptions['cameraDeviceId'] = self.getDeviceId();
232
-            }
233
-            RTCUtils.obtainAudioAndVideoPermissions(streamOptions)
316
+            if (this.resolution)
317
+                streamOptions.resolution = this.resolution;
318
+
319
+            promise = RTCUtils.obtainAudioAndVideoPermissions(streamOptions)
234 320
                 .then(function (streamsInfo) {
235
-                    var streamInfo = null;
236
-                    for(var i = 0; i < streamsInfo.length; i++) {
237
-                        if(streamsInfo[i].mediaType === self.getType()) {
238
-                            streamInfo = streamsInfo[i];
239
-                            self.stream = streamInfo.stream;
240
-                            self.track = streamInfo.track;
241
-                            // This is not good when video type changes after
242
-                            // unmute, but let's not crash here
243
-                            if (self.videoType != streamInfo.videoType) {
244
-                                logger.warn(
245
-                                    "Video type has changed after unmute!",
246
-                                    self.videoType, streamInfo.videoType);
247
-                                self.videoType = streamInfo.videoType;
248
-                            }
249
-                            break;
250
-                        }
251
-                    }
321
+                    var mediaType = self.getType();
322
+                    var streamInfo = streamsInfo.find(function(info) {
323
+                        return info.mediaType === mediaType;
324
+                    });
252 325
 
253 326
                     if(!streamInfo) {
254
-                        reject(new Error('track.no_stream_found'));
255
-                        return;
327
+                        throw new JitsiTrackError(
328
+                            JitsiTrackErrors.TRACK_NO_STREAM_FOUND);
329
+                    }else {
330
+                        self._setStream(streamInfo.stream);
331
+                        self.track = streamInfo.track;
332
+                        // This is not good when video type changes after
333
+                        // unmute, but let's not crash here
334
+                        if (self.videoType !== streamInfo.videoType) {
335
+                            logger.warn(
336
+                                "Video type has changed after unmute!",
337
+                                self.videoType, streamInfo.videoType);
338
+                            self.videoType = streamInfo.videoType;
339
+                        }
256 340
                     }
257 341
 
258
-                    for(var i = 0; i < self.containers.length; i++)
259
-                    {
260
-                        self.containers[i]
261
-                            = RTCUtils.attachMediaStream(
262
-                                    self.containers[i], self.stream);
263
-                    }
342
+                    self.containers = self.containers.map(function(cont) {
343
+                        return RTCUtils.attachMediaStream(cont, self.stream);
344
+                    });
264 345
 
265
-                    self.rtc.room.addStream(self.stream,
266
-                        function () {
267
-                            if(isAudio)
268
-                                self.rtc.room.setAudioMute(
269
-                                    mute, callbackFunction);
270
-                            else
271
-                                self.rtc.room.setVideoMute(
272
-                                    mute, callbackFunction);
273
-                        }, function (error) {
274
-                            reject(error);
275
-                        }, {
276
-                            mtype: self.type,
277
-                            type: "unmute",
278
-                            ssrc: self.ssrc,
279
-                            msid: self.getMSID()});
280
-                }).catch(function (error) {
281
-                    reject(error);
346
+                   return self._addStreamToConferenceAsUnmute();
282 347
                 });
283 348
         }
284 349
     }
350
+
351
+    return promise
352
+        .then(function() {
353
+            return self._sendMuteStatus(mute);
354
+        })
355
+        .then(function() {
356
+            self.eventEmitter.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED, this);
357
+        });
358
+};
359
+
360
+/**
361
+ * Adds stream to conference and marks it as "unmute" operation.
362
+ *
363
+ * @private
364
+ * @returns {Promise}
365
+ */
366
+JitsiLocalTrack.prototype._addStreamToConferenceAsUnmute = function () {
367
+    if (!this.conference || !this.conference.room) {
368
+        return Promise.resolve();
369
+    }
370
+
371
+    var self = this;
372
+
373
+    return new Promise(function(resolve, reject) {
374
+        self.conference.room.addStream(
375
+            self.stream,
376
+            resolve,
377
+            reject,
378
+            {
379
+                mtype: self.type,
380
+                type: "unmute",
381
+                ssrc: self.ssrc,
382
+                msid: self.getMSID()
383
+            });
384
+    });
385
+};
386
+
387
+/**
388
+ * Removes stream from conference and marks it as "mute" operation.
389
+ * @param {Function} successCallback will be called on success
390
+ * @param {Function} errorCallback will be called on error
391
+ * @private
392
+ */
393
+JitsiLocalTrack.prototype._removeStreamFromConferenceAsMute =
394
+function (successCallback, errorCallback) {
395
+    if (!this.conference || !this.conference.room) {
396
+        successCallback();
397
+        return;
398
+    }
399
+
400
+    this.conference.room.removeStream(
401
+        this.stream,
402
+        successCallback,
403
+        errorCallback,
404
+        {
405
+            mtype: this.type,
406
+            type: "mute",
407
+            ssrc: this.ssrc
408
+        });
409
+};
410
+
411
+/**
412
+ * Sends mute status for a track to conference if any.
413
+ *
414
+ * @param {boolean} mute - If track is muted.
415
+ * @private
416
+ * @returns {Promise}
417
+ */
418
+JitsiLocalTrack.prototype._sendMuteStatus = function(mute) {
419
+    if (!this.conference || !this.conference.room) {
420
+        return Promise.resolve();
421
+    }
422
+
423
+    var self = this;
424
+
425
+    return new Promise(function(resolve) {
426
+        self.conference.room[
427
+            self.isAudioTrack()
428
+                ? 'setAudioMute'
429
+                : 'setVideoMute'](mute, resolve);
430
+    });
285 431
 };
286 432
 
287 433
 /**
434
+ * @inheritdoc
435
+ *
288 436
  * Stops sending the media track. And removes it from the HTML.
289 437
  * NOTE: Works for local tracks only.
438
+ *
439
+ * @extends JitsiTrack#dispose
290 440
  * @returns {Promise}
291 441
  */
292 442
 JitsiLocalTrack.prototype.dispose = function () {
443
+    var self = this;
293 444
     var promise = Promise.resolve();
294 445
 
295 446
     if (this.conference){
@@ -297,12 +448,10 @@ JitsiLocalTrack.prototype.dispose = function () {
297 448
     }
298 449
 
299 450
     if (this.stream) {
300
-        RTCUtils.stopMediaStream(this.stream);
451
+        this._stopMediaStream();
301 452
         this.detach();
302 453
     }
303 454
 
304
-    this.disposed = true;
305
-
306 455
     RTCUtils.removeListener(RTCEvents.DEVICE_LIST_CHANGED,
307 456
         this._onDeviceListChanged);
308 457
 
@@ -311,7 +460,10 @@ JitsiLocalTrack.prototype.dispose = function () {
311 460
             this._onAudioOutputDeviceChanged);
312 461
     }
313 462
 
314
-    return promise;
463
+    return promise
464
+        .then(function() {
465
+            return JitsiTrack.prototype.dispose.call(self); // super.dispose();
466
+        });
315 467
 };
316 468
 
317 469
 /**
@@ -331,22 +483,6 @@ JitsiLocalTrack.prototype.isMuted = function () {
331 483
     }
332 484
 };
333 485
 
334
-/**
335
- * Private method. Updates rtc property of the track.
336
- * @param rtc the rtc instance.
337
- */
338
-JitsiLocalTrack.prototype._setRTC = function (rtc) {
339
-    this.rtc = rtc;
340
-    // We want to keep up with postponed events which should have been fired
341
-    // on "attach" call, but for local track we not always have the conference
342
-    // before attaching. However this may result in duplicated events if they
343
-    // have been triggered on "attach" already.
344
-    for(var i = 0; i < this.containers.length; i++)
345
-    {
346
-        this._maybeFireTrackAttached(this.containers[i]);
347
-    }
348
-};
349
-
350 486
 /**
351 487
  * Updates the SSRC associated with the MediaStream in JitsiLocalTrack object.
352 488
  * @ssrc the new ssrc
@@ -356,7 +492,6 @@ JitsiLocalTrack.prototype._setSSRC = function (ssrc) {
356 492
 };
357 493
 
358 494
 
359
-//FIXME: This dependacy is not necessary. This is quick fix.
360 495
 /**
361 496
  * Sets the JitsiConference object associated with the track. This is temp
362 497
  * solution.
@@ -364,6 +499,15 @@ JitsiLocalTrack.prototype._setSSRC = function (ssrc) {
364 499
  */
365 500
 JitsiLocalTrack.prototype._setConference = function(conference) {
366 501
     this.conference = conference;
502
+
503
+    // We want to keep up with postponed events which should have been fired
504
+    // on "attach" call, but for local track we not always have the conference
505
+    // before attaching. However this may result in duplicated events if they
506
+    // have been triggered on "attach" already.
507
+    for(var i = 0; i < this.containers.length; i++)
508
+    {
509
+        this._maybeFireTrackAttached(this.containers[i]);
510
+    }
367 511
 };
368 512
 
369 513
 /**
@@ -398,4 +542,113 @@ JitsiLocalTrack.prototype.getDeviceId = function () {
398 542
     return this._realDeviceId || this.deviceId;
399 543
 };
400 544
 
545
+/**
546
+ * Sets the value of bytes sent statistic.
547
+ * @param bytesSent {intiger} the new value
548
+ * NOTE: used only for audio tracks to detect audio issues.
549
+ */
550
+JitsiLocalTrack.prototype._setByteSent = function (bytesSent) {
551
+    this._bytesSent = bytesSent;
552
+    // FIXME it's a shame that PeerConnection and ICE status does not belong
553
+    // to the RTC module and it has to be accessed through
554
+    // the conference(and through the XMPP chat room ???) instead
555
+    let iceConnectionState
556
+        = this.conference ? this.conference.getConnectionState() : null;
557
+    if(this._testByteSent && "connected" === iceConnectionState) {
558
+        setTimeout(function () {
559
+            if(this._bytesSent <= 0){
560
+                //we are not receiving anything from the microphone
561
+                this._fireNoDataFromSourceEvent();
562
+            }
563
+        }.bind(this), 3000);
564
+        this._testByteSent = false;
565
+    }
566
+};
567
+
568
+/**
569
+ * Returns facing mode for video track from camera. For other cases (e.g. audio
570
+ * track or 'desktop' video track) returns undefined.
571
+ *
572
+ * @returns {CameraFacingMode|undefined}
573
+ */
574
+JitsiLocalTrack.prototype.getCameraFacingMode = function () {
575
+    if (this.isVideoTrack() && this.videoType === VideoType.CAMERA) {
576
+        // MediaStreamTrack#getSettings() is not implemented in many browsers,
577
+        // so we need feature checking here. Progress on the respective
578
+        // browser's implementation can be tracked at
579
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=2481 for Chromium
580
+        // and https://bugzilla.mozilla.org/show_bug.cgi?id=1213517 for Firefox.
581
+        // Even if a browser implements getSettings() already, it might still
582
+        // not return anything for 'facingMode'.
583
+        var trackSettings;
584
+
585
+        try {
586
+            trackSettings = this.track.getSettings();
587
+        } catch (e) {
588
+            // XXX React-native-webrtc, for example, defines
589
+            // MediaStreamTrack#getSettings() but the implementation throws a
590
+            // "Not implemented" Error.
591
+        }
592
+        if (trackSettings && 'facingMode' in trackSettings) {
593
+            return trackSettings.facingMode;
594
+        }
595
+
596
+        if (typeof this._facingMode !== 'undefined') {
597
+            return this._facingMode;
598
+        }
599
+
600
+        // In most cases we are showing a webcam. So if we've gotten here, it
601
+        // should be relatively safe to assume that we are probably showing
602
+        // the user-facing camera.
603
+        return CameraFacingMode.USER;
604
+    }
605
+
606
+    return undefined;
607
+};
608
+
609
+/**
610
+ * Stops the associated MediaStream.
611
+ */
612
+JitsiLocalTrack.prototype._stopMediaStream = function () {
613
+    this.stopStreamInProgress = true;
614
+    RTCUtils.stopMediaStream(this.stream);
615
+    this.stopStreamInProgress = false;
616
+};
617
+
618
+/**
619
+ * Detects camera issues on ended and mute events from MediaStreamTrack.
620
+ * @returns {boolean} true if an issue is detected and false otherwise
621
+ */
622
+JitsiLocalTrack.prototype._checkForCameraIssues = function () {
623
+    if(!this.isVideoTrack() || this.stopStreamInProgress ||
624
+        this.videoType === VideoType.DESKTOP)
625
+        return false;
626
+
627
+    return !this._isReceivingData();
628
+};
629
+
630
+/**
631
+ * Checks whether the attached MediaStream is reveiving data from source or
632
+ * not. If the stream property is null(because of mute or another reason) this
633
+ * method will return false.
634
+ * NOTE: This method doesn't indicate problem with the streams directly.
635
+ * For example in case of video mute the method will return false or if the
636
+ * user has disposed the track.
637
+ * @returns {boolean} true if the stream is receiving data and false otherwise.
638
+ */
639
+JitsiLocalTrack.prototype._isReceivingData = function () {
640
+    if(!this.stream)
641
+        return false;
642
+    // In older version of the spec there is no muted property and
643
+    // readyState can have value muted. In the latest versions
644
+    // readyState can have values "live" and "ended" and there is
645
+    // muted boolean property. If the stream is muted that means that
646
+    // we aren't receiving any data from the source. We want to notify
647
+    // the users for error if the stream is muted or ended on it's
648
+    // creation.
649
+    return this.stream.getTracks().some(track =>
650
+        ((!("readyState" in track) || track.readyState === "live")
651
+            && (!("muted" in track) || track.muted !== true)));
652
+};
653
+
401 654
 module.exports = JitsiLocalTrack;

+ 102
- 7
modules/RTC/JitsiRemoteTrack.js View File

@@ -1,9 +1,18 @@
1
+/* global Strophe */
2
+
1 3
 var JitsiTrack = require("./JitsiTrack");
2
-var JitsiTrackEvents = require("../../JitsiTrackEvents");
4
+import * as JitsiTrackEvents from "../../JitsiTrackEvents";
5
+var logger = require("jitsi-meet-logger").getLogger(__filename);
6
+var RTCBrowserType = require("./RTCBrowserType");
7
+var RTCEvents = require("../../service/RTC/RTCEvents");
8
+var Statistics = require("../statistics/statistics");
9
+
10
+var ttfmTrackerAudioAttached = false;
11
+var ttfmTrackerVideoAttached = false;
3 12
 
4 13
 /**
5 14
  * Represents a single media track (either audio or video).
6
- * @param RTC the rtc instance.
15
+ * @param rtc {RTC} the RTC service instance.
7 16
  * @param ownerJid the MUC JID of the track owner
8 17
  * @param stream WebRTC MediaStream, parent of the track
9 18
  * @param track underlying WebRTC MediaStreamTrack for new JitsiRemoteTrack
@@ -13,18 +22,52 @@ var JitsiTrackEvents = require("../../JitsiTrackEvents");
13 22
  * @param muted intial muted state of the JitsiRemoteTrack
14 23
  * @constructor
15 24
  */
16
-function JitsiRemoteTrack(RTC, ownerJid, stream, track, mediaType, videoType,
25
+function JitsiRemoteTrack(rtc, conference, ownerJid, stream, track, mediaType, videoType,
17 26
                           ssrc, muted) {
18 27
     JitsiTrack.call(
19
-        this, RTC, stream, track, function () {}, mediaType, videoType, ssrc);
20
-    this.rtc = RTC;
28
+        this, conference, stream, track, function () {}, mediaType, videoType, ssrc);
29
+    this.rtc = rtc;
21 30
     this.peerjid = ownerJid;
22 31
     this.muted = muted;
32
+    // we want to mark whether the track has been ever muted
33
+    // to detect ttfm events for startmuted conferences, as it can significantly
34
+    // increase ttfm values
35
+    this.hasBeenMuted = muted;
36
+    // Bind 'onmute' and 'onunmute' event handlers
37
+    if (this.rtc && this.track)
38
+        this._bindMuteHandlers();
23 39
 }
24 40
 
25 41
 JitsiRemoteTrack.prototype = Object.create(JitsiTrack.prototype);
26 42
 JitsiRemoteTrack.prototype.constructor = JitsiRemoteTrack;
27 43
 
44
+JitsiRemoteTrack.prototype._bindMuteHandlers = function() {
45
+    // Bind 'onmute'
46
+    // FIXME it would be better to use recently added '_setHandler' method, but
47
+    // 1. It does not allow to set more than one handler to the event
48
+    // 2. It does mix MediaStream('inactive') with MediaStreamTrack events
49
+    // 3. Allowing to bind more than one event handler requires too much
50
+    //    refactoring around camera issues detection.
51
+    this.track.addEventListener('mute', function () {
52
+
53
+        logger.debug(
54
+            '"onmute" event(' + Date.now() + '): ',
55
+            this.getParticipantId(), this.getType(), this.getSSRC());
56
+
57
+        this.rtc.eventEmitter.emit(RTCEvents.REMOTE_TRACK_MUTE, this);
58
+    }.bind(this));
59
+
60
+    // Bind 'onunmute'
61
+    this.track.addEventListener('unmute', function () {
62
+
63
+        logger.debug(
64
+            '"onunmute" event(' + Date.now() + '): ',
65
+            this.getParticipantId(), this.getType(), this.getSSRC());
66
+
67
+        this.rtc.eventEmitter.emit(RTCEvents.REMOTE_TRACK_UNMUTE, this);
68
+    }.bind(this));
69
+};
70
+
28 71
 /**
29 72
  * Sets current muted status and fires an events for the change.
30 73
  * @param value the muted status.
@@ -33,12 +76,15 @@ JitsiRemoteTrack.prototype.setMute = function (value) {
33 76
     if(this.muted === value)
34 77
         return;
35 78
 
79
+    if(value)
80
+        this.hasBeenMuted = true;
81
+
36 82
     // we can have a fake video stream
37 83
     if(this.stream)
38 84
         this.stream.muted = value;
39 85
 
40 86
     this.muted = value;
41
-    this.eventEmitter.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED);
87
+    this.eventEmitter.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED, this);
42 88
 };
43 89
 
44 90
 /**
@@ -84,6 +130,55 @@ JitsiRemoteTrack.prototype._setVideoType = function (type) {
84 130
     this.eventEmitter.emit(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, type);
85 131
 };
86 132
 
87
-delete JitsiRemoteTrack.prototype.dispose;
133
+JitsiRemoteTrack.prototype._playCallback = function () {
134
+    var type = (this.isVideoTrack() ? 'video' : 'audio');
135
+
136
+    var now = window.performance.now();
137
+    console.log("(TIME) Render " + type + ":\t", now);
138
+    this.conference.getConnectionTimes()[type + ".render"] = now;
139
+
140
+    var ttfm = now
141
+        - (this.conference.getConnectionTimes()["session.initiate"]
142
+        - this.conference.getConnectionTimes()["muc.joined"])
143
+        - (window.connectionTimes["obtainPermissions.end"]
144
+        - window.connectionTimes["obtainPermissions.start"]);
145
+    this.conference.getConnectionTimes()[type + ".ttfm"] = ttfm;
146
+    console.log("(TIME) TTFM " + type + ":\t", ttfm);
147
+    var eventName = type +'.ttfm';
148
+    if(this.hasBeenMuted)
149
+        eventName += '.muted';
150
+    Statistics.analytics.sendEvent(eventName, {value: ttfm});
151
+};
152
+
153
+/**
154
+ * Attach time to first media tracker only if there is conference and only
155
+ * for the first element.
156
+ * @param container the HTML container which can be 'video' or 'audio' element.
157
+ *        It can also be 'object' element if Temasys plugin is in use and this
158
+ *        method has been called previously on video or audio HTML element.
159
+ * @private
160
+ */
161
+JitsiRemoteTrack.prototype._attachTTFMTracker = function (container) {
162
+    if((ttfmTrackerAudioAttached && this.isAudioTrack())
163
+        || (ttfmTrackerVideoAttached && this.isVideoTrack()))
164
+        return;
165
+
166
+    if (this.isAudioTrack())
167
+        ttfmTrackerAudioAttached = true;
168
+    if (this.isVideoTrack())
169
+        ttfmTrackerVideoAttached = true;
170
+
171
+    if (RTCBrowserType.isTemasysPluginUsed()) {
172
+        // XXX Don't require Temasys unless it's to be used because it doesn't
173
+        // run on React Native, for example.
174
+        const AdapterJS = require("./adapter.screenshare");
175
+
176
+        // FIXME: this is not working for IE11
177
+        AdapterJS.addEvent(container, 'play', this._playCallback.bind(this));
178
+    }
179
+    else {
180
+        container.addEventListener("canplay", this._playCallback.bind(this));
181
+    }
182
+};
88 183
 
89 184
 module.exports = JitsiRemoteTrack;

+ 98
- 11
modules/RTC/JitsiTrack.js View File

@@ -1,12 +1,20 @@
1 1
 /* global __filename, module */
2 2
 var logger = require("jitsi-meet-logger").getLogger(__filename);
3 3
 var RTCBrowserType = require("./RTCBrowserType");
4
-var RTCEvents = require("../../service/RTC/RTCEvents");
5 4
 var RTCUtils = require("./RTCUtils");
6
-var JitsiTrackEvents = require("../../JitsiTrackEvents");
5
+import * as JitsiTrackEvents from "../../JitsiTrackEvents";
7 6
 var EventEmitter = require("events");
8 7
 var MediaType = require("../../service/RTC/MediaType");
9 8
 
9
+/**
10
+ * Maps our handler types to MediaStreamTrack properties.
11
+ */
12
+var trackHandler2Prop = {
13
+    "track_mute": "onmute",//Not supported on FF
14
+    "track_unmute": "onunmute",
15
+    "track_ended": "onended"
16
+};
17
+
10 18
 /**
11 19
  * This implements 'onended' callback normally fired by WebRTC after the stream
12 20
  * is stopped. There is no such behaviour yet in FF, so we have to add it.
@@ -54,7 +62,7 @@ function addMediaStreamInactiveHandler(mediaStream, handler) {
54 62
  * @param videoType the VideoType for this track if any
55 63
  * @param ssrc the SSRC of this track if known
56 64
  */
57
-function JitsiTrack(rtc, stream, track, streamInactiveHandler, trackMediaType,
65
+function JitsiTrack(conference, stream, track, streamInactiveHandler, trackMediaType,
58 66
                     videoType, ssrc)
59 67
 {
60 68
     /**
@@ -62,7 +70,7 @@ function JitsiTrack(rtc, stream, track, streamInactiveHandler, trackMediaType,
62 70
      * @type {Array}
63 71
      */
64 72
     this.containers = [];
65
-    this.rtc = rtc;
73
+    this.conference = conference;
66 74
     this.stream = stream;
67 75
     this.ssrc = ssrc;
68 76
     this.eventEmitter = new EventEmitter();
@@ -70,14 +78,54 @@ function JitsiTrack(rtc, stream, track, streamInactiveHandler, trackMediaType,
70 78
     this.type = trackMediaType;
71 79
     this.track = track;
72 80
     this.videoType = videoType;
81
+    this.handlers = {};
82
+
83
+    /**
84
+     * Indicates whether this JitsiTrack has been disposed. If true, this
85
+     * JitsiTrack is to be considered unusable and operations involving it are
86
+     * to fail (e.g. {@link JitsiConference#addTrack(JitsiTrack)},
87
+     * {@link JitsiConference#removeTrack(JitsiTrack)}).
88
+     * @type {boolean}
89
+     */
90
+    this.disposed = false;
91
+    this._setHandler("inactive", streamInactiveHandler);
92
+}
93
+
94
+/**
95
+ * Sets handler to the WebRTC MediaStream or MediaStreamTrack object depending
96
+ * on the passed type.
97
+ * @param {string} type the type of the handler that is going to be set
98
+ * @param {Function} handler the handler.
99
+ */
100
+JitsiTrack.prototype._setHandler = function (type, handler) {
101
+    this.handlers[type] = handler;
102
+    if(!this.stream)
103
+        return;
73 104
 
74
-    if(stream) {
105
+    if(type === "inactive") {
75 106
         if (RTCBrowserType.isFirefox()) {
76 107
             implementOnEndedHandling(this);
77 108
         }
78
-        addMediaStreamInactiveHandler(stream, streamInactiveHandler);
109
+        addMediaStreamInactiveHandler(this.stream, handler);
110
+    } else if(trackHandler2Prop.hasOwnProperty(type)) {
111
+        this.stream.getVideoTracks().forEach(function (track) {
112
+            track[trackHandler2Prop[type]] = handler;
113
+        }, this);
79 114
     }
80
-}
115
+};
116
+
117
+/**
118
+ * Sets the stream property of JitsiTrack object and sets all stored handlers
119
+ * to it.
120
+ * @param {MediaStream} stream the new stream.
121
+ */
122
+JitsiTrack.prototype._setStream = function (stream) {
123
+    this.stream = stream;
124
+    Object.keys(this.handlers).forEach(function (type) {
125
+        typeof(this.handlers[type]) === "function" &&
126
+            this._setHandler(type, this.handlers[type]);
127
+    }, this);
128
+};
81 129
 
82 130
 /**
83 131
  * Returns the type (audio or video) of this track.
@@ -93,6 +141,16 @@ JitsiTrack.prototype.isAudioTrack = function () {
93 141
     return this.getType() === MediaType.AUDIO;
94 142
 };
95 143
 
144
+/**
145
+ * Checks whether the underlying WebRTC <tt>MediaStreamTrack</tt> is muted
146
+ * according to it's 'muted' field status.
147
+ * @return {boolean} <tt>true</tt> if the underlying <tt>MediaStreamTrack</tt>
148
+ * is muted or <tt>false</tt> otherwise.
149
+ */
150
+JitsiTrack.prototype.isWebRTCTrackMuted = function () {
151
+    return this.track && this.track.muted;
152
+};
153
+
96 154
 /**
97 155
  * Check if this is videotrack.
98 156
  */
@@ -100,6 +158,15 @@ JitsiTrack.prototype.isVideoTrack = function () {
100 158
     return this.getType() === MediaType.VIDEO;
101 159
 };
102 160
 
161
+/**
162
+ * Checks whether this is a local track.
163
+ * @abstract
164
+ * @return {boolean} 'true' if it's a local track or 'false' otherwise.
165
+ */
166
+JitsiTrack.prototype.isLocal = function () {
167
+    throw new Error("Not implemented by subclass");
168
+};
169
+
103 170
 /**
104 171
  * Returns the WebRTC MediaStream instance.
105 172
  */
@@ -151,8 +218,8 @@ JitsiTrack.prototype.getUsageLabel = function () {
151 218
  * @private
152 219
  */
153 220
 JitsiTrack.prototype._maybeFireTrackAttached = function (container) {
154
-    if (this.rtc && container) {
155
-        this.rtc.eventEmitter.emit(RTCEvents.TRACK_ATTACHED, this, container);
221
+    if (this.conference && container) {
222
+        this.conference._onTrackAttach(this, container);
156 223
     }
157 224
 };
158 225
 
@@ -180,6 +247,8 @@ JitsiTrack.prototype.attach = function (container) {
180 247
 
181 248
     this._maybeFireTrackAttached(container);
182 249
 
250
+    this._attachTTFMTracker(container);
251
+
183 252
     return container;
184 253
 };
185 254
 
@@ -208,10 +277,28 @@ JitsiTrack.prototype.detach = function (container) {
208 277
 };
209 278
 
210 279
 /**
211
- * Dispose sending the media track. And removes it from the HTML.
212
- * NOTE: Works for local tracks only.
280
+ * Attach time to first media tracker only if there is conference and only
281
+ * for the first element.
282
+ * @param container the HTML container which can be 'video' or 'audio' element.
283
+ *        It can also be 'object' element if Temasys plugin is in use and this
284
+ *        method has been called previously on video or audio HTML element.
285
+ * @private
286
+ */
287
+// eslint-disable-next-line no-unused-vars
288
+JitsiTrack.prototype._attachTTFMTracker = function (container) {
289
+};
290
+
291
+/**
292
+ * Removes attached event listeners.
293
+ *
294
+ * @returns {Promise}
213 295
  */
214 296
 JitsiTrack.prototype.dispose = function () {
297
+    this.eventEmitter.removeAllListeners();
298
+
299
+    this.disposed = true;
300
+
301
+    return Promise.resolve();
215 302
 };
216 303
 
217 304
 /**

+ 183
- 88
modules/RTC/RTC.js View File

@@ -1,11 +1,12 @@
1
-/* global __filename, APP, module */
1
+/* global Strophe */
2
+
2 3
 var logger = require("jitsi-meet-logger").getLogger(__filename);
3 4
 var EventEmitter = require("events");
4
-var RTCBrowserType = require("./RTCBrowserType");
5 5
 var RTCEvents = require("../../service/RTC/RTCEvents.js");
6 6
 var RTCUtils = require("./RTCUtils.js");
7
-var JitsiTrack = require("./JitsiTrack");
8 7
 var JitsiLocalTrack = require("./JitsiLocalTrack.js");
8
+import JitsiTrackError from "../../JitsiTrackError";
9
+import * as JitsiTrackErrors from "../../JitsiTrackErrors";
9 10
 var DataChannels = require("./DataChannels");
10 11
 var JitsiRemoteTrack = require("./JitsiRemoteTrack.js");
11 12
 var MediaType = require("../../service/RTC/MediaType");
@@ -17,23 +18,26 @@ function createLocalTracks(tracksInfo, options) {
17 18
     var deviceId = null;
18 19
     tracksInfo.forEach(function(trackInfo){
19 20
         if (trackInfo.mediaType === MediaType.AUDIO) {
20
-          deviceId = options.micDeviceId;
21
+            deviceId = options.micDeviceId;
21 22
         } else if (trackInfo.videoType === VideoType.CAMERA){
22
-          deviceId = options.cameraDeviceId;
23
+            deviceId = options.cameraDeviceId;
23 24
         }
24 25
         var localTrack
25 26
             = new JitsiLocalTrack(
26 27
                 trackInfo.stream,
27 28
                 trackInfo.track,
28 29
                 trackInfo.mediaType,
29
-                trackInfo.videoType, trackInfo.resolution, deviceId);
30
+                trackInfo.videoType,
31
+                trackInfo.resolution,
32
+                deviceId,
33
+                options.facingMode);
30 34
         newTracks.push(localTrack);
31 35
     });
32 36
     return newTracks;
33 37
 }
34 38
 
35
-function RTC(room, options) {
36
-    this.room = room;
39
+function RTC(conference, options) {
40
+    this.conference = conference;
37 41
     this.localTracks = [];
38 42
     //FIXME: We should support multiple streams per jid.
39 43
     this.remoteTracks = {};
@@ -42,24 +46,11 @@ function RTC(room, options) {
42 46
     this.eventEmitter = new EventEmitter();
43 47
     var self = this;
44 48
     this.options = options || {};
45
-    room.addPresenceListener("videomuted", function (values, from) {
46
-        var videoTrack = self.getRemoteVideoTrack(from);
47
-        if (videoTrack) {
48
-            videoTrack.setMute(values.value == "true");
49
-        }
50
-    });
51
-    room.addPresenceListener("audiomuted", function (values, from) {
52
-        var audioTrack = self.getRemoteAudioTrack(from);
53
-        if (audioTrack) {
54
-            audioTrack.setMute(values.value == "true");
55
-        }
56
-    });
57
-    room.addPresenceListener("videoType", function(data, from) {
58
-        var videoTrack = self.getRemoteVideoTrack(from);
59
-        if (videoTrack) {
60
-            videoTrack._setVideoType(data.value);
61
-        }
62
-    });
49
+    // A flag whether we had received that the data channel had opened
50
+    // we can get this flag out of sync if for some reason data channel got
51
+    // closed from server, a desired behaviour so we can see errors when this
52
+    // happen
53
+    this.dataChannelsOpen = false;
63 54
 
64 55
     // Switch audio output device on all remote audio tracks. Local audio tracks
65 56
     // handle this event by themselves.
@@ -94,61 +85,89 @@ function RTC(room, options) {
94 85
 RTC.obtainAudioAndVideoPermissions = function (options) {
95 86
     return RTCUtils.obtainAudioAndVideoPermissions(options).then(
96 87
         function (tracksInfo) {
97
-            return createLocalTracks(tracksInfo, options);
88
+            var tracks = createLocalTracks(tracksInfo, options);
89
+            return !tracks.some(track => !track._isReceivingData())? tracks :
90
+                Promise.reject(new JitsiTrackError(
91
+                    JitsiTrackErrors.NO_DATA_FROM_SOURCE));
98 92
     });
99 93
 };
100 94
 
101 95
 RTC.prototype.onIncommingCall = function(event) {
102
-    if(this.options.config.openSctp)
96
+    if(this.options.config.openSctp) {
103 97
         this.dataChannels = new DataChannels(event.peerconnection,
104 98
             this.eventEmitter);
105
-    // Add local Tracks to the ChatRoom
106
-    this.localTracks.forEach(function(localTrack) {
107
-        var ssrcInfo = null;
108
-        if(localTrack.isVideoTrack() && localTrack.isMuted()) {
109
-            /**
110
-             * Handles issues when the stream is added before the peerconnection
111
-             * is created. The peerconnection is created when second participant
112
-             * enters the call. In that use case the track doesn't have
113
-             * information about it's ssrcs and no jingle packets are sent. That
114
-             * can cause inconsistent behavior later.
115
-             *
116
-             * For example:
117
-             * If we mute the stream and than second participant enter it's
118
-             * remote SDP won't include that track. On unmute we are not sending
119
-             * any jingle packets which will brake the unmute.
120
-             *
121
-             * In order to solve issues like the above one here we have to
122
-             * generate the ssrc information for the track .
123
-             */
124
-            localTrack._setSSRC(
125
-                this.room.generateNewStreamSSRCInfo());
126
-            ssrcInfo = {
127
-                mtype: localTrack.getType(),
128
-                type: "addMuted",
129
-                ssrc: localTrack.ssrc,
130
-                msid: localTrack.initialMSID
131
-            };
132
-        }
133
-        try {
134
-            this.room.addStream(
135
-                localTrack.getOriginalStream(), function () {}, function () {},
136
-                ssrcInfo, true);
137
-        } catch(e) {
138
-            GlobalOnErrorHandler.callErrorHandler(e);
139
-            logger.error(e);
140
-        }
141
-    }.bind(this));
99
+        this._dataChannelOpenListener = () => {
100
+            // mark that dataChannel is opened
101
+            this.dataChannelsOpen = true;
102
+            // when the data channel becomes available, tell the bridge
103
+            // about video selections so that it can do adaptive simulcast,
104
+            // we want the notification to trigger even if userJid
105
+            // is undefined, or null.
106
+            // XXX why do we not do the same for pinned endpoints?
107
+            try {
108
+                this.dataChannels.sendSelectedEndpointMessage(
109
+                    this.selectedEndpoint);
110
+            } catch (error) {
111
+                GlobalOnErrorHandler.callErrorHandler(error);
112
+                logger.error("Cannot sendSelectedEndpointMessage ",
113
+                    this.selectedEndpoint, ". Error: ", error);
114
+            }
115
+
116
+            this.removeListener(RTCEvents.DATA_CHANNEL_OPEN,
117
+                this._dataChannelOpenListener);
118
+            this._dataChannelOpenListener = null;
119
+        };
120
+        this.addListener(RTCEvents.DATA_CHANNEL_OPEN,
121
+            this._dataChannelOpenListener);
122
+    }
123
+};
124
+
125
+/**
126
+ * Should be called when current media session ends and after the PeerConnection
127
+ * has been closed using PeerConnection.close() method.
128
+ */
129
+RTC.prototype.onCallEnded = function() {
130
+    if (this.dataChannels) {
131
+        // DataChannels are not explicitly closed as the PeerConnection
132
+        // is closed on call ended which triggers data channel onclose events.
133
+        // The reference is cleared to disable any logic related to the data
134
+        // channels.
135
+        this.dataChannels = null;
136
+        this.dataChannelsOpen = false;
137
+    }
142 138
 };
143 139
 
140
+/**
141
+ * Elects the participant with the given id to be the selected participant in
142
+ * order to always receive video for this participant (even when last n is
143
+ * enabled).
144
+ * If there is no data channel we store it and send it through the channel once
145
+ * it is created.
146
+ * @param id {string} the user id.
147
+ * @throws NetworkError or InvalidStateError or Error if the operation fails.
148
+*/
144 149
 RTC.prototype.selectEndpoint = function (id) {
145
-    if(this.dataChannels)
150
+    // cache the value if channel is missing, till we open it
151
+    this.selectedEndpoint = id;
152
+    if(this.dataChannels && this.dataChannelsOpen)
146 153
         this.dataChannels.sendSelectedEndpointMessage(id);
147 154
 };
148 155
 
156
+/**
157
+ * Elects the participant with the given id to be the pinned participant in
158
+ * order to always receive video for this participant (even when last n is
159
+ * enabled).
160
+ * @param id {string} the user id
161
+ * @throws NetworkError or InvalidStateError or Error if the operation fails.
162
+ */
149 163
 RTC.prototype.pinEndpoint = function (id) {
150
-    if(this.dataChannels)
164
+    if(this.dataChannels) {
151 165
         this.dataChannels.sendPinnedEndpointMessage(id);
166
+    } else {
167
+        // FIXME: cache value while there is no data channel created
168
+        // and send the cached state once channel is created
169
+        throw new Error("Data channels support is disabled!");
170
+    }
152 171
 };
153 172
 
154 173
 RTC.prototype.addListener = function (type, listener) {
@@ -164,7 +183,7 @@ RTC.addListener = function (eventType, listener) {
164 183
 };
165 184
 
166 185
 RTC.removeListener = function (eventType, listener) {
167
-    RTCUtils.removeListener(eventType, listener)
186
+    RTCUtils.removeListener(eventType, listener);
168 187
 };
169 188
 
170 189
 RTC.isRTCReady = function () {
@@ -185,7 +204,8 @@ RTC.prototype.addLocalTrack = function (track) {
185 204
         throw new Error('track must not be null nor undefined');
186 205
 
187 206
     this.localTracks.push(track);
188
-    track._setRTC(this);
207
+
208
+    track.conference = this.conference;
189 209
 
190 210
     if (track.isAudioTrack()) {
191 211
         this.localAudio = track;
@@ -203,18 +223,29 @@ RTC.prototype.getLocalVideoTrack = function () {
203 223
 };
204 224
 
205 225
 /**
206
- * Gets JitsiRemoteTrack for AUDIO MediaType associated with given MUC nickname
207
- * (resource part of the JID).
226
+ * Gets JitsiRemoteTrack for the passed MediaType associated with given MUC
227
+ * nickname (resource part of the JID).
228
+ * @param type audio or video.
208 229
  * @param resource the resource part of the MUC JID
209 230
  * @returns {JitsiRemoteTrack|null}
210 231
  */
211
-RTC.prototype.getRemoteAudioTrack = function (resource) {
232
+RTC.prototype.getRemoteTrackByType = function (type, resource) {
212 233
     if (this.remoteTracks[resource])
213
-        return this.remoteTracks[resource][MediaType.AUDIO];
234
+        return this.remoteTracks[resource][type];
214 235
     else
215 236
         return null;
216 237
 };
217 238
 
239
+/**
240
+ * Gets JitsiRemoteTrack for AUDIO MediaType associated with given MUC nickname
241
+ * (resource part of the JID).
242
+ * @param resource the resource part of the MUC JID
243
+ * @returns {JitsiRemoteTrack|null}
244
+ */
245
+RTC.prototype.getRemoteAudioTrack = function (resource) {
246
+    return this.getRemoteTrackByType(MediaType.AUDIO, resource);
247
+};
248
+
218 249
 /**
219 250
  * Gets JitsiRemoteTrack for VIDEO MediaType associated with given MUC nickname
220 251
  * (resource part of the JID).
@@ -222,10 +253,7 @@ RTC.prototype.getRemoteAudioTrack = function (resource) {
222 253
  * @returns {JitsiRemoteTrack|null}
223 254
  */
224 255
 RTC.prototype.getRemoteVideoTrack = function (resource) {
225
-    if (this.remoteTracks[resource])
226
-        return this.remoteTracks[resource][MediaType.VIDEO];
227
-    else
228
-        return null;
256
+    return this.getRemoteTrackByType(MediaType.VIDEO, resource);
229 257
 };
230 258
 
231 259
 /**
@@ -272,7 +300,7 @@ RTC.prototype.removeLocalTrack = function (track) {
272 300
 RTC.prototype.createRemoteTrack = function (event) {
273 301
     var ownerJid = event.owner;
274 302
     var remoteTrack = new JitsiRemoteTrack(
275
-        this,  ownerJid, event.stream,    event.track,
303
+        this, this.conference, ownerJid, event.stream, event.track,
276 304
         event.mediaType, event.videoType, event.ssrc, event.muted);
277 305
     var resource = Strophe.getResourceFromJid(ownerJid);
278 306
     var remoteTracks
@@ -287,17 +315,40 @@ RTC.prototype.createRemoteTrack = function (event) {
287 315
 
288 316
 /**
289 317
  * Removes all JitsiRemoteTracks associated with given MUC nickname (resource
290
- * part of the JID).
291
- * @param resource the resource part of the MUC JID
292
- * @returns {JitsiRemoteTrack|null}
318
+ * part of the JID). Returns array of removed tracks.
319
+ *
320
+ * @param {string} resource - The resource part of the MUC JID.
321
+ * @returns {JitsiRemoteTrack[]}
293 322
  */
294 323
 RTC.prototype.removeRemoteTracks = function (resource) {
295
-    var remoteTracks = this.remoteTracks[resource];
324
+    var removedTracks = [];
325
+    var removedAudioTrack = this.removeRemoteTrack(resource, MediaType.AUDIO);
326
+    var removedVideoTrack = this.removeRemoteTrack(resource, MediaType.VIDEO);
327
+
328
+    removedAudioTrack && removedTracks.push(removedAudioTrack);
329
+    removedVideoTrack && removedTracks.push(removedVideoTrack);
296 330
 
297
-    if(remoteTracks) {
298
-        remoteTracks['audio'] && remoteTracks['audio'].dispose();
299
-        remoteTracks['video'] && remoteTracks['video'].dispose();
300
-        delete this.remoteTracks[resource];
331
+    delete this.remoteTracks[resource];
332
+
333
+    return removedTracks;
334
+};
335
+
336
+/**
337
+ * Removes specified track type associated with given MUC nickname
338
+ * (resource part of the JID). Returns removed track if any.
339
+ *
340
+ * @param {string} resource - The resource part of the MUC JID.
341
+ * @param {string} mediaType - Type of track to remove.
342
+ * @returns {JitsiRemoteTrack|undefined}
343
+ */
344
+RTC.prototype.removeRemoteTrack = function (resource, mediaType) {
345
+    var remoteTracksForResource = this.remoteTracks[resource];
346
+
347
+    if (remoteTracksForResource && remoteTracksForResource[mediaType]) {
348
+        var track = remoteTracksForResource[mediaType];
349
+        track.dispose();
350
+        delete remoteTracksForResource[mediaType];
351
+        return track;
301 352
     }
302 353
 };
303 354
 
@@ -415,7 +466,10 @@ RTC.isDesktopSharingEnabled = function () {
415 466
  * Closes all currently opened data channels.
416 467
  */
417 468
 RTC.prototype.closeAllDataChannels = function () {
418
-    this.dataChannels.closeAllChannels();
469
+    if(this.dataChannels) {
470
+        this.dataChannels.closeAllChannels();
471
+        this.dataChannelsOpen = false;
472
+    }
419 473
 };
420 474
 
421 475
 RTC.prototype.dispose = function() {
@@ -457,7 +511,7 @@ RTC.prototype.setAudioLevel = function (resource, audioLevel) {
457 511
 RTC.prototype.getResourceBySSRC = function (ssrc) {
458 512
     if((this.localVideo && ssrc == this.localVideo.getSSRC())
459 513
         || (this.localAudio && ssrc == this.localAudio.getSSRC())) {
460
-        return Strophe.getResourceFromJid(this.room.myroomjid);
514
+        return this.conference.myUserId();
461 515
     }
462 516
 
463 517
     var track = this.getRemoteTrackBySSRC(ssrc);
@@ -482,4 +536,45 @@ RTC.prototype.getRemoteTrackBySSRC = function (ssrc) {
482 536
     return null;
483 537
 };
484 538
 
539
+/**
540
+ * Handles remote track mute / unmute events.
541
+ * @param type {string} "audio" or "video"
542
+ * @param isMuted {boolean} the new mute state
543
+ * @param from {string} user id
544
+ */
545
+RTC.prototype.handleRemoteTrackMute = function (type, isMuted, from) {
546
+    var track = this.getRemoteTrackByType(type, from);
547
+    if (track) {
548
+        track.setMute(isMuted);
549
+    }
550
+};
551
+
552
+/**
553
+ * Handles remote track video type events
554
+ * @param value {string} the new video type
555
+ * @param from {string} user id
556
+ */
557
+RTC.prototype.handleRemoteTrackVideoTypeChanged = function (value, from) {
558
+    var videoTrack = this.getRemoteVideoTrack(from);
559
+    if (videoTrack) {
560
+        videoTrack._setVideoType(value);
561
+    }
562
+};
563
+
564
+/**
565
+ * Sends message via the datachannels.
566
+ * @param to {string} the id of the endpoint that should receive the message.
567
+ * If "" the message will be sent to all participants.
568
+ * @param payload {object} the payload of the message.
569
+ * @throws NetworkError or InvalidStateError or Error if the operation fails
570
+ * or there is no data channel created
571
+ */
572
+RTC.prototype.sendDataChannelMessage = function (to, payload) {
573
+    if(this.dataChannels) {
574
+        this.dataChannels.sendDataChannelMessage(to, payload);
575
+    } else {
576
+        throw new Error("Data channels support is disabled!");
577
+    }
578
+};
579
+
485 580
 module.exports = RTC;

+ 47
- 4
modules/RTC/RTCBrowserType.js View File

@@ -30,6 +30,18 @@ var RTCBrowserType = {
30 30
         return currentBrowser;
31 31
     },
32 32
 
33
+    /**
34
+     * Gets current browser name, split from the type.
35
+     * @returns {string}
36
+     */
37
+    getBrowserName: function () {
38
+        var browser = currentBrowser.split('rtc_browser.')[1];
39
+        if (RTCBrowserType.isAndroid()) {
40
+            browser = 'android';
41
+        }
42
+        return browser;
43
+    },
44
+
33 45
     /**
34 46
      * Checks if current browser is Chrome.
35 47
      * @returns {boolean}
@@ -94,6 +106,16 @@ var RTCBrowserType = {
94 106
         return RTCBrowserType.isIExplorer() || RTCBrowserType.isSafari();
95 107
     },
96 108
 
109
+    /**
110
+     * Checks if the current browser triggers 'onmute'/'onunmute' events when
111
+     * user's connection is interrupted and the video stops playback.
112
+     * @returns {*|boolean} 'true' if the event is supported or 'false'
113
+     * otherwise.
114
+     */
115
+    isVideoMuteOnConnInterruptedSupported: function () {
116
+        return RTCBrowserType.isChrome();
117
+    },
118
+
97 119
     /**
98 120
      * Returns Firefox version.
99 121
      * @returns {number|null}
@@ -125,6 +147,14 @@ var RTCBrowserType = {
125 147
      */
126 148
     isAndroid: function() {
127 149
         return isAndroid;
150
+    },
151
+
152
+    /**
153
+     * Whether jitsi-meet supports simulcast on the current browser.
154
+     * @returns {boolean}
155
+     */
156
+    supportsSimulcast: function() {
157
+        return RTCBrowserType.isChrome();
128 158
     }
129 159
 
130 160
     // Add version getters for other browsers when needed
@@ -222,13 +252,26 @@ function detectReactNative() {
222 252
     var match
223 253
         = navigator.userAgent.match(/\b(react[ \t_-]*native)(?:\/(\S+))?/i);
224 254
     var version;
225
-    if (match) {
255
+    // If we're remote debugging a React Native app, it may be treated as
256
+    // Chrome. Check navigator.product as well and always return some version
257
+    // even if we can't get the real one.
258
+    if (match || navigator.product === 'ReactNative') {
226 259
         currentBrowser = RTCBrowserType.RTC_BROWSER_REACT_NATIVE;
227
-        if (match.length > 2) {
260
+        var name;
261
+        if (match && match.length > 2) {
262
+            name = match[1];
228 263
             version = match[2];
229 264
         }
230
-        console.info(
231
-            "This appears to be " + /* name */ match[1] + ", ver: " + version);
265
+        if (!name) {
266
+            name = 'react-native';
267
+        }
268
+        if (!version) {
269
+            version = 'unknown';
270
+        }
271
+        console.info('This appears to be ' + name + ', ver: ' + version);
272
+    } else {
273
+        // We're not running in a React Native environment.
274
+        version = null;
232 275
     }
233 276
     return version;
234 277
 }

+ 3
- 3
modules/RTC/RTCUIHelper.js View File

@@ -1,7 +1,7 @@
1
-/* global $, __filename */
1
+/* global $ */
2
+
2 3
 var logger = require("jitsi-meet-logger").getLogger(__filename);
3 4
 var RTCBrowserType = require("./RTCBrowserType");
4
-var RTC = require('./RTC');
5 5
 
6 6
 var RTCUIHelper = {
7 7
 
@@ -58,7 +58,7 @@ var RTCUIHelper = {
58 58
      */
59 59
     setAutoPlay: function (streamElement, autoPlay) {
60 60
         if (!RTCBrowserType.isIExplorer()) {
61
-            streamElement.autoplay = true;
61
+            streamElement.autoplay = autoPlay;
62 62
         }
63 63
     }
64 64
 };

+ 168
- 56
modules/RTC/RTCUtils.js View File

@@ -1,24 +1,37 @@
1
-/* global config, require, attachMediaStream, getUserMedia,
2
-   RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, MediaStreamTrack,
3
-   mozRTCPeerConnection, mozRTCSessionDescription, mozRTCIceCandidate,
4
-   webkitRTCPeerConnection, webkitMediaStream, webkitURL
1
+/* global $,
2
+          attachMediaStream,
3
+          MediaStreamTrack,
4
+          RTCIceCandidate,
5
+          RTCPeerConnection,
6
+          RTCSessionDescription,
7
+          mozRTCIceCandidate,
8
+          mozRTCPeerConnection,
9
+          mozRTCSessionDescription,
10
+          webkitMediaStream,
11
+          webkitRTCPeerConnection,
12
+          webkitURL
5 13
 */
6
-/* jshint -W101 */
7 14
 
8 15
 var logger = require("jitsi-meet-logger").getLogger(__filename);
9 16
 var RTCBrowserType = require("./RTCBrowserType");
10 17
 var Resolutions = require("../../service/RTC/Resolutions");
11 18
 var RTCEvents = require("../../service/RTC/RTCEvents");
12
-var AdapterJS = require("./adapter.screenshare");
13 19
 var SDPUtil = require("../xmpp/SDPUtil");
14 20
 var EventEmitter = require("events");
15 21
 var screenObtainer = require("./ScreenObtainer");
16
-var JitsiTrackErrors = require("../../JitsiTrackErrors");
17
-var JitsiTrackError = require("../../JitsiTrackError");
22
+import JitsiTrackError from "../../JitsiTrackError";
18 23
 var MediaType = require("../../service/RTC/MediaType");
19 24
 var VideoType = require("../../service/RTC/VideoType");
25
+var CameraFacingMode = require("../../service/RTC/CameraFacingMode");
20 26
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
21 27
 
28
+// XXX Don't require Temasys unless it's to be used because it doesn't run on
29
+// React Native, for example.
30
+const AdapterJS
31
+    = RTCBrowserType.isTemasysPluginUsed()
32
+        ? require("./adapter.screenshare")
33
+        : undefined;
34
+
22 35
 var eventEmitter = new EventEmitter();
23 36
 
24 37
 var AVAILABLE_DEVICES_POLL_INTERVAL_TIME = 3000; // ms
@@ -31,6 +44,8 @@ var devices = {
31 44
 // Currently audio output device change is supported only in Chrome and
32 45
 // default output always has 'default' device ID
33 46
 var audioOutputDeviceId = 'default'; // default device
47
+// whether user has explicitly set a device to use
48
+var audioOutputChanged = false;
34 49
 // Disables Acoustic Echo Cancellation
35 50
 var disableAEC = false;
36 51
 // Disables Noise Suppression
@@ -42,20 +57,33 @@ var isAudioOutputDeviceChangeAvailable =
42 57
 
43 58
 var currentlyAvailableMediaDevices;
44 59
 
45
-var rawEnumerateDevicesWithCallback = navigator.mediaDevices
46
-    && navigator.mediaDevices.enumerateDevices
60
+var rawEnumerateDevicesWithCallback = undefined;
61
+/**
62
+ * "rawEnumerateDevicesWithCallback" will be initialized only after WebRTC is
63
+ * ready. Otherwise it is too early to assume that the devices listing is not
64
+ * supported.
65
+ */
66
+function initRawEnumerateDevicesWithCallback() {
67
+    rawEnumerateDevicesWithCallback = navigator.mediaDevices
68
+        && navigator.mediaDevices.enumerateDevices
47 69
         ? function(callback) {
48
-            navigator.mediaDevices.enumerateDevices().then(callback, function () {
49
-                callback([]);
70
+            navigator.mediaDevices.enumerateDevices().then(
71
+                callback, function () {
72
+                    callback([]);
50 73
             });
51 74
         }
75
+        // Safari:
76
+        // "ReferenceError: Can't find variable: MediaStreamTrack"
77
+        // when Temasys plugin is not installed yet, have to delay this call
78
+        // until WebRTC is ready.
52 79
         : (MediaStreamTrack && MediaStreamTrack.getSources)
53
-            ? function (callback) {
54
-                MediaStreamTrack.getSources(function (sources) {
55
-                    callback(sources.map(convertMediaStreamTrackSource));
56
-                });
57
-            }
58
-            : undefined;
80
+        ? function (callback) {
81
+            MediaStreamTrack.getSources(function (sources) {
82
+                callback(sources.map(convertMediaStreamTrackSource));
83
+            });
84
+        }
85
+        : undefined;
86
+}
59 87
 
60 88
 // TODO: currently no browser supports 'devicechange' event even in nightly
61 89
 // builds so no feature/browser detection is used at all. However in future this
@@ -103,7 +131,7 @@ function setResolutionConstraints(constraints, resolution) {
103 131
  * @param {string} options.desktopStream
104 132
  * @param {string} options.cameraDeviceId
105 133
  * @param {string} options.micDeviceId
106
- * @param {'user'|'environment'} options.facingMode
134
+ * @param {CameraFacingMode} options.facingMode
107 135
  * @param {bool} firefox_fake_device
108 136
  */
109 137
 function getConstraints(um, options) {
@@ -140,12 +168,13 @@ function getConstraints(um, options) {
140 168
             // TODO: Maybe use "exact" syntax if options.facingMode is defined,
141 169
             // but this probably needs to be decided when updating other
142 170
             // constraints, as we currently don't use "exact" syntax anywhere.
171
+            var facingMode = options.facingMode || CameraFacingMode.USER;
172
+
143 173
             if (isNewStyleConstraintsSupported) {
144
-                constraints.video.facingMode = options.facingMode || 'user';
174
+                constraints.video.facingMode = facingMode;
145 175
             }
146
-
147 176
             constraints.video.optional.push({
148
-                facingMode: options.facingMode || 'user'
177
+                facingMode: facingMode
149 178
             });
150 179
         }
151 180
 
@@ -280,12 +309,21 @@ function getConstraints(um, options) {
280 309
     return constraints;
281 310
 }
282 311
 
283
-function setAvailableDevices(um, available) {
312
+/**
313
+ * Sets the availbale devices based on the options we requested and the
314
+ * streams we received.
315
+ * @param um the options we requested to getUserMedia.
316
+ * @param stream the stream we received from calling getUserMedia.
317
+ */
318
+function setAvailableDevices(um, stream) {
319
+    var audioTracksReceived = stream && !!stream.getAudioTracks().length;
320
+    var videoTracksReceived = stream && !!stream.getVideoTracks().length;
321
+
284 322
     if (um.indexOf("video") != -1) {
285
-        devices.video = available;
323
+        devices.video = videoTracksReceived;
286 324
     }
287 325
     if (um.indexOf("audio") != -1) {
288
-        devices.audio = available;
326
+        devices.audio = audioTracksReceived;
289 327
     }
290 328
 
291 329
     eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, devices);
@@ -346,8 +384,8 @@ function pollForAvailableMediaDevices() {
346 384
  * @param {MediaDeviceInfo[]} devices - list of media devices.
347 385
  * @emits RTCEvents.DEVICE_LIST_CHANGED
348 386
  */
349
-function onMediaDevicesListChanged(devices) {
350
-    currentlyAvailableMediaDevices = devices.slice(0);
387
+function onMediaDevicesListChanged(devicesReceived) {
388
+    currentlyAvailableMediaDevices = devicesReceived.slice(0);
351 389
     logger.info('list of media devices has changed:', currentlyAvailableMediaDevices);
352 390
 
353 391
     var videoInputDevices = currentlyAvailableMediaDevices.filter(function (d) {
@@ -367,15 +405,15 @@ function onMediaDevicesListChanged(devices) {
367 405
 
368 406
     if (videoInputDevices.length &&
369 407
         videoInputDevices.length === videoInputDevicesWithEmptyLabels.length) {
370
-        setAvailableDevices(['video'], false);
408
+        devices.video = false;
371 409
     }
372 410
 
373 411
     if (audioInputDevices.length &&
374 412
         audioInputDevices.length === audioInputDevicesWithEmptyLabels.length) {
375
-        setAvailableDevices(['audio'], false);
413
+        devices.audio = false;
376 414
     }
377 415
 
378
-    eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, devices);
416
+    eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, devicesReceived);
379 417
 }
380 418
 
381 419
 // In case of IE we continue from 'onReady' callback
@@ -386,6 +424,9 @@ function onReady (options, GUM) {
386 424
     eventEmitter.emit(RTCEvents.RTC_READY, true);
387 425
     screenObtainer.init(options, GUM);
388 426
 
427
+    // Initialize rawEnumerateDevicesWithCallback
428
+    initRawEnumerateDevicesWithCallback();
429
+
389 430
     if (RTCUtils.isDeviceListAvailable() && rawEnumerateDevicesWithCallback) {
390 431
         rawEnumerateDevicesWithCallback(function (devices) {
391 432
             currentlyAvailableMediaDevices = devices.splice(0);
@@ -557,6 +598,7 @@ function handleLocalStream(streams, resolution) {
557 598
         if (audioVideo) {
558 599
             var audioTracks = audioVideo.getAudioTracks();
559 600
             if (audioTracks.length) {
601
+                // eslint-disable-next-line new-cap
560 602
                 audioStream = new webkitMediaStream();
561 603
                 for (var i = 0; i < audioTracks.length; i++) {
562 604
                     audioStream.addTrack(audioTracks[i]);
@@ -565,6 +607,7 @@ function handleLocalStream(streams, resolution) {
565 607
 
566 608
             var videoTracks = audioVideo.getVideoTracks();
567 609
             if (videoTracks.length) {
610
+                // eslint-disable-next-line new-cap
568 611
                 videoStream = new webkitMediaStream();
569 612
                 for (var j = 0; j < videoTracks.length; j++) {
570 613
                     videoStream.addTrack(videoTracks[j]);
@@ -623,7 +666,9 @@ function wrapAttachMediaStream(origAttachMediaStream) {
623 666
         if (stream
624 667
                 && RTCUtils.isDeviceChangeAvailable('output')
625 668
                 && stream.getAudioTracks
626
-                && stream.getAudioTracks().length) {
669
+                && stream.getAudioTracks().length
670
+                // we skip setting audio output if there was no explicit change
671
+                && audioOutputChanged) {
627 672
             element.setSinkId(RTCUtils.getAudioOutputDevice())
628 673
                 .catch(function (ex) {
629 674
                     var err = new JitsiTrackError(ex, null, ['audiooutput']);
@@ -639,7 +684,7 @@ function wrapAttachMediaStream(origAttachMediaStream) {
639 684
         }
640 685
 
641 686
         return res;
642
-    }
687
+    };
643 688
 }
644 689
 
645 690
 /**
@@ -746,8 +791,8 @@ var RTCUtils = {
746 791
                     }
747 792
                     return SDPUtil.filter_special_chars(id);
748 793
                 };
749
-                RTCSessionDescription = mozRTCSessionDescription;
750
-                RTCIceCandidate = mozRTCIceCandidate;
794
+                RTCSessionDescription = mozRTCSessionDescription; // eslint-disable-line
795
+                RTCIceCandidate = mozRTCIceCandidate;             // eslint-disable-line
751 796
             } else if (RTCBrowserType.isChrome() ||
752 797
                     RTCBrowserType.isOpera() ||
753 798
                     RTCBrowserType.isNWJS() ||
@@ -811,7 +856,7 @@ var RTCUtils = {
811 856
                 //AdapterJS.WebRTCPlugin.setLogLevel(
812 857
                 //    AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
813 858
                 var self = this;
814
-                AdapterJS.webRTCReady(function (isPlugin) {
859
+                AdapterJS.webRTCReady(function () {
815 860
 
816 861
                     self.peerconnection = RTCPeerConnection;
817 862
                     self.getUserMedia = window.getUserMedia;
@@ -844,22 +889,20 @@ var RTCUtils = {
844 889
                         return SDPUtil.filter_special_chars(stream.label);
845 890
                     };
846 891
 
847
-                    onReady(options, self.getUserMediaWithConstraints);
892
+                    onReady(options,
893
+                        self.getUserMediaWithConstraints.bind(self));
848 894
                     resolve();
849 895
                 });
850 896
             } else {
851 897
                 var errmsg = 'Browser does not appear to be WebRTC-capable';
852
-                try {
853
-                    logger.error(errmsg);
854
-                } catch (e) {
855
-                }
898
+                logger.error(errmsg);
856 899
                 reject(new Error(errmsg));
857 900
                 return;
858 901
             }
859 902
 
860 903
             // Call onReady() if Temasys plugin is not used
861 904
             if (!RTCBrowserType.isTemasysPluginUsed()) {
862
-                onReady(options, this.getUserMediaWithConstraints);
905
+                onReady(options, this.getUserMediaWithConstraints.bind(this));
863 906
                 resolve();
864 907
             }
865 908
         }.bind(this));
@@ -878,7 +921,6 @@ var RTCUtils = {
878 921
     **/
879 922
     getUserMediaWithConstraints: function ( um, success_callback, failure_callback, options) {
880 923
         options = options || {};
881
-        var resolution = options.resolution;
882 924
         var constraints = getConstraints(um, options);
883 925
 
884 926
         logger.info("Get media constraints", constraints);
@@ -887,11 +929,11 @@ var RTCUtils = {
887 929
             this.getUserMedia(constraints,
888 930
                 function (stream) {
889 931
                     logger.log('onUserMediaSuccess');
890
-                    setAvailableDevices(um, true);
932
+                    setAvailableDevices(um, stream);
891 933
                     success_callback(stream);
892 934
                 },
893 935
                 function (error) {
894
-                    setAvailableDevices(um, false);
936
+                    setAvailableDevices(um, undefined);
895 937
                     logger.warn('Failed to get access to local media. Error ',
896 938
                         error, constraints);
897 939
 
@@ -925,6 +967,7 @@ var RTCUtils = {
925 967
         var self = this;
926 968
 
927 969
         options = options || {};
970
+        var dsOptions = options.desktopSharingExtensionExternalInstallation;
928 971
         return new Promise(function (resolve, reject) {
929 972
             var successCallback = function (stream) {
930 973
                 resolve(handleLocalStream(stream, options.resolution));
@@ -955,7 +998,8 @@ var RTCUtils = {
955 998
 
956 999
                 if(screenObtainer.isSupported()){
957 1000
                     deviceGUM["desktop"] = screenObtainer.obtainStream.bind(
958
-                        screenObtainer);
1001
+                        screenObtainer,
1002
+                        dsOptions);
959 1003
                 }
960 1004
                 // With FF/IE we can't split the stream into audio and video because FF
961 1005
                 // doesn't support media stream constructors. So, we need to get the
@@ -1007,15 +1051,40 @@ var RTCUtils = {
1007 1051
                                     devices.push("video");
1008 1052
                                 }
1009 1053
 
1010
-                                reject(new JitsiTrackError(
1011
-                                    { name: "UnknownError" },
1012
-                                    getConstraints(options.devices, options),
1013
-                                    devices)
1014
-                                );
1054
+                                // we are missing one of the media we requested
1055
+                                // in order to get the actual error that caused
1056
+                                // this missing media we will call one more time
1057
+                                // getUserMedia so we can obtain the actual
1058
+                                // error (Example usecases are requesting
1059
+                                // audio and video and video device is missing
1060
+                                // or device is denied to be used and chrome is
1061
+                                // set to not ask for permissions)
1062
+                                self.getUserMediaWithConstraints(
1063
+                                    devices,
1064
+                                    function () {
1065
+                                        // we already failed to obtain this
1066
+                                        // media, so we are not supposed in any
1067
+                                        // way to receive success for this call
1068
+                                        // any way we will throw an error to be
1069
+                                        // sure the promise will finish
1070
+                                        reject(new JitsiTrackError(
1071
+                                            { name: "UnknownError" },
1072
+                                            getConstraints(
1073
+                                                options.devices, options),
1074
+                                            devices)
1075
+                                        );
1076
+                                    },
1077
+                                    function (error) {
1078
+                                        // rejects with real error for not
1079
+                                        // obtaining the media
1080
+                                        reject(error);
1081
+                                    },options);
1082
+
1015 1083
                                 return;
1016 1084
                             }
1017 1085
                             if(hasDesktop) {
1018 1086
                                 screenObtainer.obtainStream(
1087
+                                    dsOptions,
1019 1088
                                     function (desktopStream) {
1020 1089
                                         successCallback({audioVideo: stream,
1021 1090
                                             desktopStream: desktopStream});
@@ -1034,6 +1103,7 @@ var RTCUtils = {
1034 1103
                         options);
1035 1104
                 } else if (hasDesktop) {
1036 1105
                     screenObtainer.obtainStream(
1106
+                        dsOptions,
1037 1107
                         function (stream) {
1038 1108
                             successCallback({desktopStream: stream});
1039 1109
                         }, function (error) {
@@ -1055,17 +1125,51 @@ var RTCUtils = {
1055 1125
     isRTCReady: function () {
1056 1126
         return rtcReady;
1057 1127
     },
1058
-    /**
1059
-     * Checks if its possible to enumerate available cameras/micropones.
1060
-     * @returns {boolean} true if available, false otherwise.
1061
-     */
1062
-    isDeviceListAvailable: function () {
1128
+    _isDeviceListAvailable: function () {
1129
+        if (!rtcReady)
1130
+            throw new Error("WebRTC not ready yet");
1063 1131
         var isEnumerateDevicesAvailable
1064 1132
             = navigator.mediaDevices && navigator.mediaDevices.enumerateDevices;
1065 1133
         if (isEnumerateDevicesAvailable) {
1066 1134
             return true;
1067 1135
         }
1068
-        return (MediaStreamTrack && MediaStreamTrack.getSources)? true : false;
1136
+        return (typeof MediaStreamTrack !== "undefined" &&
1137
+            MediaStreamTrack.getSources)? true : false;
1138
+    },
1139
+    /**
1140
+     * Returns a promise which can be used to make sure that the WebRTC stack
1141
+     * has been initialized.
1142
+     *
1143
+     * @returns {Promise} which is resolved only if the WebRTC stack is ready.
1144
+     * Note that currently we do not detect stack initialization failure and
1145
+     * the promise is never rejected(unless unexpected error occurs).
1146
+     */
1147
+    onRTCReady: function() {
1148
+        if (rtcReady) {
1149
+            return Promise.resolve();
1150
+        } else {
1151
+            return new Promise(function (resolve) {
1152
+                var listener = function () {
1153
+                    eventEmitter.removeListener(RTCEvents.RTC_READY, listener);
1154
+                    resolve();
1155
+                };
1156
+                eventEmitter.addListener(RTCEvents.RTC_READY, listener);
1157
+                // We have no failed event, so... it either resolves or nothing
1158
+                // happens
1159
+            });
1160
+        }
1161
+    },
1162
+    /**
1163
+     * Checks if its possible to enumerate available cameras/microphones.
1164
+     *
1165
+     * @returns {Promise<boolean>} a Promise which will be resolved only once
1166
+     * the WebRTC stack is ready, either with true if the device listing is
1167
+     * available available or with false otherwise.
1168
+     */
1169
+    isDeviceListAvailable: function () {
1170
+        return this.onRTCReady().then(function() {
1171
+            return this._isDeviceListAvailable();
1172
+        }.bind(this));
1069 1173
     },
1070 1174
     /**
1071 1175
      * Returns true if changing the input (camera / microphone) or output
@@ -1101,6 +1205,13 @@ var RTCUtils = {
1101 1205
             mediaStream.stop();
1102 1206
         }
1103 1207
 
1208
+        // The MediaStream implementation of the react-native-webrtc project has
1209
+        // an explicit release method that is to be invoked in order to release
1210
+        // used resources such as memory.
1211
+        if (mediaStream.release) {
1212
+            mediaStream.release();
1213
+        }
1214
+
1104 1215
         // if we have done createObjectURL, lets clean it
1105 1216
         var url = mediaStream.jitsiObjectURL;
1106 1217
         if (url) {
@@ -1132,6 +1243,7 @@ var RTCUtils = {
1132 1243
         return featureDetectionAudioEl.setSinkId(deviceId)
1133 1244
             .then(function() {
1134 1245
                 audioOutputDeviceId = deviceId;
1246
+                audioOutputChanged = true;
1135 1247
 
1136 1248
                 logger.log('Audio output device set to ' + deviceId);
1137 1249
 

+ 154
- 55
modules/RTC/ScreenObtainer.js View File

@@ -1,11 +1,10 @@
1 1
 /* global chrome, $, alert */
2
-/* jshint -W003 */
2
+
3
+var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
3 4
 var logger = require("jitsi-meet-logger").getLogger(__filename);
4 5
 var RTCBrowserType = require("./RTCBrowserType");
5
-var AdapterJS = require("./adapter.screenshare");
6
-var JitsiTrackErrors = require("../../JitsiTrackErrors");
7
-var JitsiTrackError = require("../../JitsiTrackError");
8
-var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
6
+import JitsiTrackError from "../../JitsiTrackError";
7
+import * as JitsiTrackErrors from "../../JitsiTrackErrors";
9 8
 
10 9
 /**
11 10
  * Indicates whether the Chrome desktop sharing extension is installed.
@@ -36,11 +35,25 @@ var reDetectFirefoxExtension = false;
36 35
 
37 36
 var GUM = null;
38 37
 
38
+/**
39
+ * The error returned by chrome when trying to start inline installation from
40
+ * popup.
41
+ */
42
+const CHROME_EXTENSION_POPUP_ERROR
43
+    = "Inline installs can not be initiated from pop-up windows.";
44
+
45
+/**
46
+ * The error message returned by chrome when the extension is installed.
47
+ */
48
+const CHROME_NO_EXTENSION_ERROR_MSG // eslint-disable-line no-unused-vars
49
+    = "Could not establish connection. Receiving end does not exist.";
50
+
39 51
 /**
40 52
  * Handles obtaining a stream from a screen capture on different browsers.
41 53
  */
42 54
 var ScreenObtainer = {
43 55
     obtainStream: null,
56
+
44 57
     /**
45 58
      * Initializes the function used to obtain a screen capture
46 59
      * (this.obtainStream).
@@ -52,8 +65,10 @@ var ScreenObtainer = {
52 65
      * or disable screen capture (if the value is other).
53 66
      * Note that for the "screen" media source to work the
54 67
      * 'chrome://flags/#enable-usermedia-screen-capture' flag must be set.
68
+     * @param options {object}
69
+     * @param gum {Function} GUM method
55 70
      */
56
-    init: function(options, gum) {
71
+    init(options, gum) {
57 72
         var obtainDesktopStream = null;
58 73
         this.options = options = options || {};
59 74
         GUM = gum;
@@ -66,14 +81,43 @@ var ScreenObtainer = {
66 81
             (options.desktopSharingChromeMethod || options.desktopSharing);
67 82
 
68 83
         if (RTCBrowserType.isNWJS()) {
69
-            obtainDesktopStream = function (onSuccess, onFailure) {
84
+            obtainDesktopStream = (options, onSuccess, onFailure) => {
70 85
                 window.JitsiMeetNW.obtainDesktopStream (
71
-                    onSuccess, function (error, constraints) {
72
-                        onFailure && onFailure(new JitsiTrackError(
73
-                            error, constraints, ["desktop"]));
86
+                    onSuccess,
87
+                    (error, constraints) => {
88
+                        var jitsiError;
89
+                        // FIXME:
90
+                        // This is very very durty fix for recognising that the
91
+                        // user have clicked the cancel button from the Desktop
92
+                        // sharing pick window. The proper solution would be to
93
+                        // detect this in the NWJS application by checking the
94
+                        // streamId === "". Even better solution would be to
95
+                        // stop calling GUM from the NWJS app and just pass the
96
+                        // streamId to lib-jitsi-meet. This way the desktop
97
+                        // sharing implementation for NWJS and chrome extension
98
+                        // will be the same and lib-jitsi-meet will be able to
99
+                        // control the constraints, check the streamId, etc.
100
+                        //
101
+                        // I cannot find documentation about "InvalidStateError"
102
+                        // but this is what we are receiving from GUM when the
103
+                        // streamId for the desktop sharing is "".
104
+                        if (error && error.name == "InvalidStateError") {
105
+                            jitsiError = new JitsiTrackError(
106
+                                JitsiTrackErrors.CHROME_EXTENSION_USER_CANCELED
107
+                            );
108
+                        } else {
109
+                            jitsiError = new JitsiTrackError(
110
+                                error, constraints, ["desktop"]);
111
+                        }
112
+                        (typeof(onFailure) === "function") &&
113
+                            onFailure(jitsiError);
74 114
                     });
75 115
             };
76 116
         } else if (RTCBrowserType.isTemasysPluginUsed()) {
117
+            // XXX Don't require Temasys unless it's to be used because it
118
+            // doesn't run on React Native, for example.
119
+            const AdapterJS = require("./adapter.screenshare");
120
+
77 121
             if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
78 122
                 logger.info("Screensharing not supported by this plugin " +
79 123
                     "version");
@@ -109,7 +153,6 @@ var ScreenObtainer = {
109 153
             } else {
110 154
                 obtainDesktopStream = this.obtainScreenOnFirefox;
111 155
             }
112
-
113 156
         }
114 157
 
115 158
         if (!obtainDesktopStream) {
@@ -124,17 +167,16 @@ var ScreenObtainer = {
124 167
      * environment.
125 168
      * @returns {boolean}
126 169
      */
127
-    isSupported: function() {
170
+    isSupported() {
128 171
         return !!this.obtainStream;
129 172
     },
173
+
130 174
     /**
131 175
      * Obtains a screen capture stream on Firefox.
132 176
      * @param callback
133 177
      * @param errorCallback
134 178
      */
135
-    obtainScreenOnFirefox:
136
-           function (callback, errorCallback) {
137
-        var self = this;
179
+    obtainScreenOnFirefox(options, callback, errorCallback) {
138 180
         var extensionRequired = false;
139 181
         if (this.options.desktopSharingFirefoxMaxVersionExtRequired === -1 ||
140 182
             (this.options.desktopSharingFirefoxMaxVersionExtRequired >= 0 &&
@@ -146,7 +188,7 @@ var ScreenObtainer = {
146 188
         }
147 189
 
148 190
         if (!extensionRequired || firefoxExtInstalled === true) {
149
-            obtainWebRTCScreen(callback, errorCallback);
191
+            obtainWebRTCScreen(options, callback, errorCallback);
150 192
             return;
151 193
         }
152 194
 
@@ -159,13 +201,12 @@ var ScreenObtainer = {
159 201
         // extension if it hasn't.
160 202
         if (firefoxExtInstalled === null) {
161 203
             window.setTimeout(
162
-                function() {
204
+                () => {
163 205
                     if (firefoxExtInstalled === null)
164 206
                         firefoxExtInstalled = false;
165
-                    self.obtainScreenOnFirefox(callback, errorCallback);
207
+                    this.obtainScreenOnFirefox(callback, errorCallback);
166 208
                 },
167
-                300
168
-            );
209
+                300);
169 210
             logger.log("Waiting for detection of jidesha on firefox to " +
170 211
                 "finish.");
171 212
             return;
@@ -182,12 +223,12 @@ var ScreenObtainer = {
182 223
         errorCallback(
183 224
             new JitsiTrackError(JitsiTrackErrors.FIREFOX_EXTENSION_NEEDED));
184 225
     },
226
+
185 227
     /**
186 228
      * Asks Chrome extension to call chooseDesktopMedia and gets chrome
187 229
      * 'desktop' stream for returned stream token.
188 230
      */
189
-    obtainScreenFromExtension: function (streamCallback, failCallback) {
190
-        var self = this;
231
+    obtainScreenFromExtension(options, streamCallback, failCallback) {
191 232
         if (chromeExtInstalled) {
192 233
             doGetStreamFromExtension(this.options, streamCallback,
193 234
                 failCallback);
@@ -201,39 +242,71 @@ var ScreenObtainer = {
201 242
             try {
202 243
                 chrome.webstore.install(
203 244
                     getWebStoreInstallUrl(this.options),
204
-                    function (arg) {
245
+                    arg => {
205 246
                         logger.log("Extension installed successfully", arg);
206 247
                         chromeExtInstalled = true;
207
-                        // We need to give a moment for the endpoint to become
208
-                        // available
209
-                        window.setTimeout(function () {
210
-                            doGetStreamFromExtension(self.options,
211
-                                streamCallback, failCallback);
212
-                        }, 500);
248
+                        // We need to give a moment to the endpoint to become
249
+                        // available.
250
+                        waitForExtensionAfterInstall(this.options, 200, 10)
251
+                            .then(() => {
252
+                                doGetStreamFromExtension(this.options,
253
+                                    streamCallback, failCallback);
254
+                            }).catch(() => {
255
+                                this.handleExtensionInstallationError(options,
256
+                                    streamCallback, failCallback);
257
+                            });
213 258
                     },
214
-                    handleExtensionInstallationError
259
+                    this.handleExtensionInstallationError.bind(this,
260
+                        options, streamCallback, failCallback)
215 261
                 );
216 262
             } catch(e) {
217
-                handleExtensionInstallationError(e);
263
+                this.handleExtensionInstallationError(options, streamCallback,
264
+                    failCallback, e);
218 265
             }
219 266
         }
267
+    },
268
+
269
+    handleExtensionInstallationError(options, streamCallback, failCallback, e) {
270
+        const webStoreInstallUrl = getWebStoreInstallUrl(this.options);
271
+
272
+        if (CHROME_EXTENSION_POPUP_ERROR === e
273
+                && options.interval > 0
274
+                && typeof(options.checkAgain) === "function"
275
+                && typeof(options.listener) === "function") {
276
+            options.listener("waitingForExtension", webStoreInstallUrl);
277
+            this.checkForChromeExtensionOnInterval(options, streamCallback,
278
+                failCallback, e);
279
+            return;
280
+        }
220 281
 
221
-        function handleExtensionInstallationError(e) {
222
-            var msg = "Failed to install the extension from "
223
-                + getWebStoreInstallUrl(self.options);
282
+        const msg
283
+            = "Failed to install the extension from " + webStoreInstallUrl;
224 284
 
225
-            logger.log(msg, e);
285
+        logger.log(msg, e);
286
+        failCallback(new JitsiTrackError(
287
+            JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR,
288
+            msg));
289
+    },
226 290
 
291
+    checkForChromeExtensionOnInterval(options, streamCallback, failCallback) {
292
+        if (options.checkAgain() === false) {
227 293
             failCallback(new JitsiTrackError(
228
-                JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR,
229
-                msg
230
-            ));
294
+                JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR));
295
+            return;
231 296
         }
297
+        waitForExtensionAfterInstall(this.options, options.interval, 1)
298
+            .then(() => {
299
+                chromeExtInstalled = true;
300
+                options.listener("extensionFound");
301
+                this.obtainScreenFromExtension(options,
302
+                    streamCallback, failCallback);
303
+            }).catch(() => {
304
+                this.checkForChromeExtensionOnInterval(options,
305
+                    streamCallback, failCallback);
306
+            });
232 307
     }
233 308
 };
234 309
 
235
-
236
-
237 310
 /**
238 311
  * Obtains a desktop stream using getUserMedia.
239 312
  * For this to work on Chrome, the
@@ -243,12 +316,8 @@ var ScreenObtainer = {
243 316
  * 'media.getusermedia.screensharing.allowed_domains' preference in
244 317
  * 'about:config'.
245 318
  */
246
-function obtainWebRTCScreen(streamCallback, failCallback) {
247
-    GUM(
248
-        ['screen'],
249
-        streamCallback,
250
-        failCallback
251
-    );
319
+function obtainWebRTCScreen(options, streamCallback, failCallback) {
320
+    GUM(['screen'], streamCallback, failCallback);
252 321
 }
253 322
 
254 323
 /**
@@ -313,7 +382,7 @@ function checkChromeExtInstalled(callback, options) {
313 382
         //TODO: remove chromeExtensionId (deprecated)
314 383
         (options.desktopSharingChromeExtId || options.chromeExtensionId),
315 384
         { getVersion: true },
316
-        function (response) {
385
+        response => {
317 386
             if (!response || !response.version) {
318 387
                 // Communication failure - assume that no endpoint exists
319 388
                 logger.warn(
@@ -347,7 +416,7 @@ function doGetStreamFromExtension(options, streamCallback, failCallback) {
347 416
             sources: (options.desktopSharingChromeSources ||
348 417
                 options.desktopSharingSources)
349 418
         },
350
-        function (response) {
419
+        response => {
351 420
             if (!response) {
352 421
                 // possibly re-wraping error message to make code consistent
353 422
                 var lastError = chrome.runtime.lastError;
@@ -362,11 +431,9 @@ function doGetStreamFromExtension(options, streamCallback, failCallback) {
362 431
             if (response.streamId) {
363 432
                 GUM(
364 433
                     ['desktop'],
365
-                    function (stream) {
366
-                        streamCallback(stream);
367
-                    },
434
+                    stream => streamCallback(stream),
368 435
                     failCallback,
369
-                    {desktopStream: response.streamId});
436
+                    { desktopStream: response.streamId });
370 437
             } else {
371 438
                 // As noted in Chrome Desktop Capture API:
372 439
                 // If user didn't select any source (i.e. canceled the prompt)
@@ -405,7 +472,7 @@ function initChromeExtension(options) {
405 472
     // Initialize Chrome extension inline installs
406 473
     initInlineInstalls(options);
407 474
     // Check if extension is installed
408
-    checkChromeExtInstalled(function (installed, updateRequired) {
475
+    checkChromeExtInstalled((installed, updateRequired) => {
409 476
         chromeExtInstalled = installed;
410 477
         chromeExtUpdateRequired = updateRequired;
411 478
         logger.info(
@@ -414,6 +481,38 @@ function initChromeExtension(options) {
414 481
     }, options);
415 482
 }
416 483
 
484
+/**
485
+ * Checks "retries" times on every "waitInterval"ms whether the ext is alive.
486
+ * @param {Object} options the options passed to ScreanObtainer.obtainStream
487
+ * @param {int} waitInterval the number of ms between retries
488
+ * @param {int} retries the number of retries
489
+ * @returns {Promise} returns promise that will be resolved when the extension
490
+ * is alive and rejected if the extension is not alive even after "retries"
491
+ * checks
492
+ */
493
+function waitForExtensionAfterInstall(options, waitInterval, retries) {
494
+    if(retries === 0) {
495
+        return Promise.reject();
496
+    }
497
+    return new Promise((resolve, reject) => {
498
+        let currentRetries = retries;
499
+        let interval = window.setInterval(() => {
500
+            checkChromeExtInstalled( (installed) => {
501
+                if(installed) {
502
+                    window.clearInterval(interval);
503
+                    resolve();
504
+                } else {
505
+                    currentRetries--;
506
+                    if(currentRetries === 0) {
507
+                        reject();
508
+                        window.clearInterval(interval);
509
+                    }
510
+                }
511
+            }, options);
512
+        }, waitInterval);
513
+    });
514
+}
515
+
417 516
 /**
418 517
  * Starts the detection of an installed jidesha extension for firefox.
419 518
  * @param options supports "desktopSharingFirefoxDisabled",
@@ -431,11 +530,11 @@ function initFirefoxExtensionDetection(options) {
431 530
     }
432 531
 
433 532
     var img = document.createElement('img');
434
-    img.onload = function(){
533
+    img.onload = () => {
435 534
         logger.log("Detected firefox screen sharing extension.");
436 535
         firefoxExtInstalled = true;
437 536
     };
438
-    img.onerror = function(){
537
+    img.onerror = () => {
439 538
         logger.log("Detected lack of firefox screen sharing extension.");
440 539
         firefoxExtInstalled = false;
441 540
     };

+ 3503
- 401
modules/RTC/adapter.screenshare.js
File diff suppressed because it is too large
View File


+ 110
- 0
modules/TalkMutedDetection.js View File

@@ -0,0 +1,110 @@
1
+import * as JitsiConferenceEvents from "../JitsiConferenceEvents";
2
+
3
+export default class TalkMutedDetection {
4
+    /**
5
+     * Creates TalkMutedDetection
6
+     * @param conference the JitsiConference instance that created us.
7
+     * @param callback the callback to call when detected that the local user is
8
+     * talking while her microphone is muted.
9
+     * @constructor
10
+     */
11
+    constructor(conference, callback) {
12
+        /**
13
+         * The callback to call when detected that the local user is talking
14
+         * while her microphone is muted.
15
+         *
16
+         * @private
17
+         */
18
+        this._callback = callback;
19
+
20
+        /**
21
+         * The indicator which determines whether <tt>callback</tt> has been
22
+         * invoked for the current local audio track of <tt>conference</tt> so
23
+         * that it is invoked once only.
24
+         *
25
+         * @private
26
+         */
27
+        this._eventFired = false;
28
+
29
+        // XXX I went back and forth on the subject of where to put the access
30
+        // to statistics. On the one had, (1) statistics is likely intended to
31
+        // be private to conference and (2) there is a desire to keep the
32
+        // dependencies of modules to the minimum (i.e. not have
33
+        // TalkMutedDetection depend on statistics). On the other hand, (1)
34
+        // statistics is technically not private because
35
+        // JitsiConferenceEventManager accesses it and (2) TalkMutedDetection
36
+        // works exactly because it knows that there are no audio levels for
37
+        // JitsiLocalTrack but there are audio levels for the local participant
38
+        // through statistics.
39
+        conference.statistics.addAudioLevelListener(
40
+            this._audioLevel.bind(this));
41
+
42
+        conference.on(
43
+            JitsiConferenceEvents.TRACK_MUTE_CHANGED,
44
+            this._trackMuteChanged.bind(this));
45
+        conference.on(
46
+            JitsiConferenceEvents.TRACK_ADDED,
47
+            this._trackAdded.bind(this));
48
+    }
49
+
50
+    /**
51
+     * Receives audio level events for all send and receive streams.
52
+     *
53
+     * @param ssrc - The synchronization source identifier (SSRC) of the
54
+     * endpoint/participant/stream being reported.
55
+     * @param {number} audioLevel - The audio level of <tt>ssrc</tt>.
56
+     * @param {boolean} isLocal - <tt>true</tt> if <tt>ssrc</tt> represents a
57
+     * local/send stream or <tt>false</tt> for a remote/receive stream.
58
+     */
59
+    _audioLevel(ssrc, audioLevel, isLocal) {
60
+        // We are interested in the local audio stream only and if event is not
61
+        // sent yet.
62
+        if (!isLocal || !this.audioTrack || this._eventFired)
63
+            return;
64
+
65
+        if (this.audioTrack.isMuted() && audioLevel > 0.6) {
66
+            this._eventFired = true;
67
+            this._callback();
68
+        }
69
+    }
70
+
71
+    /**
72
+     * Determines whether a specific {@link JitsiTrack} represents a local audio
73
+     * track.
74
+     *
75
+     * @param {JitsiTrack} track - The <tt>JitsiTrack</tt> to be checked whether
76
+     * it represents a local audio track.
77
+     * @private
78
+     * @return {boolean} - <tt>true</tt> if the specified <tt>track</tt>
79
+     * represents a local audio track; otherwise, <tt>false</tt>.
80
+     */
81
+    _isLocalAudioTrack(track) {
82
+        return track.isAudioTrack() && track.isLocal();
83
+    }
84
+
85
+    /**
86
+     * Notifies this <tt>TalkMutedDetection</tt> that a {@link JitsiTrack} was
87
+     * added to the associated {@link JitsiConference}. Looks for the local
88
+     * audio track only.
89
+     *
90
+     * @param {JitsiTrack} track - The added <tt>JitsiTrack</tt>.
91
+     * @private
92
+     */
93
+    _trackAdded(track) {
94
+        if (this._isLocalAudioTrack(track))
95
+            this.audioTrack = track;
96
+    }
97
+
98
+    /**
99
+     * Notifies this <tt>TalkMutedDetection</tt> that the mute state of a
100
+     * {@link JitsiTrack} has changed. Looks for the local audio track only.
101
+     *
102
+     * @param {JitsiTrack} track - The <tt>JitsiTrack</tt> whose mute state has
103
+     * changed.
104
+     * @private
105
+     */
106
+    _trackMuteChanged(track) {
107
+        if (this._isLocalAudioTrack(track) && track.isMuted())
108
+            this._eventFired = false;
109
+    }
110
+}

+ 453
- 0
modules/connectivity/ConnectionQuality.js View File

@@ -0,0 +1,453 @@
1
+import * as ConnectionQualityEvents
2
+    from "../../service/connectivity/ConnectionQualityEvents";
3
+import * as ConferenceEvents from "../../JitsiConferenceEvents";
4
+import {getLogger} from "jitsi-meet-logger";
5
+import RTCBrowserType from "../RTC/RTCBrowserType";
6
+
7
+var XMPPEvents = require('../../service/xmpp/XMPPEvents');
8
+var MediaType = require('../../service/RTC/MediaType');
9
+var VideoType = require('../../service/RTC/VideoType');
10
+var Resolutions = require("../../service/RTC/Resolutions");
11
+
12
+const logger = getLogger(__filename);
13
+
14
+/**
15
+ * The value to use for the "type" field for messages sent by ConnectionQuality
16
+ * over the data channel.
17
+ */
18
+const STATS_MESSAGE_TYPE = "stats";
19
+
20
+/**
21
+ * See media/engine/simulcast.ss from webrtc.org
22
+ */
23
+const kSimulcastFormats = [
24
+    { width: 1920, height: 1080, layers:3, max: 5000, target: 4000, min: 800 },
25
+    { width: 1280, height: 720,  layers:3, max: 2500, target: 2500, min: 600 },
26
+    { width: 960,  height: 540,  layers:3, max: 900,  target: 900, min: 450 },
27
+    { width: 640,  height: 360,  layers:2, max: 700,  target: 500, min: 150 },
28
+    { width: 480,  height: 270,  layers:2, max: 450,  target: 350, min: 150 },
29
+    { width: 320,  height: 180,  layers:1, max: 200,  target: 150, min: 30 }
30
+];
31
+
32
+/**
33
+ * The initial bitrate for video in kbps.
34
+ */
35
+var startBitrate = 800;
36
+
37
+/**
38
+ * Gets the expected bitrate (in kbps) in perfect network conditions.
39
+ * @param simulcast {boolean} whether simulcast is enabled or not.
40
+ * @param resolution {Resolution} the resolution.
41
+ * @param millisSinceStart {number} the number of milliseconds since sending
42
+ * video started.
43
+ */
44
+function getTarget(simulcast, resolution, millisSinceStart) {
45
+    // Completely ignore the bitrate in the first 5 seconds, as the first
46
+    // event seems to fire very early and the value is suspicious and causes
47
+    // false positives.
48
+    if (millisSinceStart < 5000) {
49
+        return 1;
50
+    }
51
+
52
+    let target = 0;
53
+    let height = Math.min(resolution.height, resolution.width);
54
+
55
+    if (simulcast) {
56
+        // Find the first format with height no bigger than ours.
57
+        let simulcastFormat = kSimulcastFormats.find(f => f.height <= height);
58
+        if (simulcastFormat) {
59
+            // Sum the target fields from all simulcast layers for the given
60
+            // resolution (e.g. 720p + 360p + 180p).
61
+            for (height = simulcastFormat.height; height >= 180; height /=2) {
62
+                simulcastFormat
63
+                    = kSimulcastFormats.find(f => f.height == height);
64
+                if (simulcastFormat) {
65
+                    target += simulcastFormat.target;
66
+                } else {
67
+                    break;
68
+                }
69
+            }
70
+        }
71
+    } else {
72
+        // See GetMaxDefaultVideoBitrateKbps in
73
+        // media/engine/webrtcvideoengine2.cc from webrtc.org
74
+        let pixels = resolution.width * resolution.height;
75
+        if (pixels <= 320 * 240) {
76
+            target = 600;
77
+        } else if (pixels <= 640 * 480) {
78
+            target =  1700;
79
+        } else if (pixels <= 960 * 540) {
80
+            target = 2000;
81
+        } else {
82
+            target = 2500;
83
+        }
84
+    }
85
+
86
+    // Allow for an additional 1 second for ramp up -- delay any initial drop
87
+    // of connection quality by 1 second.
88
+    return Math.min(target, rampUp(Math.max(0, millisSinceStart - 1000)));
89
+}
90
+
91
+/**
92
+ * Gets the bitrate to which GCC would have ramped up in perfect network
93
+ * conditions after millisSinceStart milliseconds.
94
+ * @param millisSinceStart {number} the number of milliseconds since sending
95
+ * video was enabled.
96
+ */
97
+function rampUp(millisSinceStart) {
98
+    if (millisSinceStart > 60000) {
99
+        return Number.MAX_SAFE_INTEGER;
100
+    }
101
+    // According to GCC the send side bandwidth estimation grows with at most
102
+    // 8% per second.
103
+    // https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02#section-5.5
104
+    return startBitrate * Math.pow(1.08, millisSinceStart / 1000);
105
+}
106
+
107
+/**
108
+ * A class which monitors the local statistics coming from the RTC modules, and
109
+ * calculates a "connection quality" value, in percent, for the media
110
+ * connection. A value of 100% indicates a very good network connection, and a
111
+ * value of 0% indicates a poor connection.
112
+ */
113
+export default class ConnectionQuality {
114
+    constructor(conference, eventEmitter, options) {
115
+        this.eventEmitter = eventEmitter;
116
+
117
+        /**
118
+         * The owning JitsiConference.
119
+         */
120
+        this._conference = conference;
121
+
122
+        /**
123
+         * Whether simulcast is supported. Note that even if supported, it is
124
+         * currently not used for screensharing, which is why we have an
125
+         * additional check.
126
+         */
127
+        this._simulcast
128
+            = !options.disableSimulcast && RTCBrowserType.supportsSimulcast();
129
+
130
+        /**
131
+         * Holds statistics about the local connection quality.
132
+         */
133
+        this._localStats = {connectionQuality: 100};
134
+
135
+        /**
136
+         * The time this._localStats.connectionQuality was last updated.
137
+         */
138
+        this._lastConnectionQualityUpdate = -1;
139
+
140
+        /**
141
+         * Maps a participant ID to an object holding connection quality
142
+         * statistics received from this participant.
143
+         */
144
+        this._remoteStats = {};
145
+
146
+        /**
147
+         * The time that the ICE state last changed to CONNECTED. We use this
148
+         * to calculate how much time we as a sender have had to ramp-up.
149
+         */
150
+        this._timeIceConnected = -1;
151
+
152
+        /**
153
+         * The time that local video was unmuted. We use this to calculate how
154
+         * much time we as a sender have had to ramp-up.
155
+         */
156
+        this._timeVideoUnmuted = -1;
157
+
158
+
159
+        // We assume a global startBitrate value for the sake of simplicity.
160
+        if (options.startBitrate && options.startBitrate > 0) {
161
+            startBitrate = options.startBitrate;
162
+        }
163
+
164
+        // TODO: consider ignoring these events and letting the user of
165
+        // lib-jitsi-meet handle these separately.
166
+        conference.on(
167
+            ConferenceEvents.CONNECTION_INTERRUPTED,
168
+            () => {
169
+                this._updateLocalConnectionQuality(0);
170
+                this.eventEmitter.emit(
171
+                    ConnectionQualityEvents.LOCAL_STATS_UPDATED,
172
+                    this._localStats);
173
+                this._broadcastLocalStats();
174
+            });
175
+
176
+        conference.room.addListener(
177
+            XMPPEvents.ICE_CONNECTION_STATE_CHANGED,
178
+            (newState) => {
179
+                if (newState === 'connected') {
180
+                    this._timeIceConnected = window.performance.now();
181
+                }
182
+            });
183
+
184
+        // Listen to DataChannel message from other participants in the
185
+        // conference, and update the _remoteStats field accordingly.
186
+        conference.on(
187
+            ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
188
+            (participant, payload) => {
189
+                if (payload.type === STATS_MESSAGE_TYPE) {
190
+                    this._updateRemoteStats(
191
+                        participant.getId(), payload.values);
192
+                }
193
+        });
194
+
195
+        // Listen to local statistics events originating from the RTC module
196
+        // and update the _localStats field.
197
+        // Oh, and by the way, the resolutions of all remote participants are
198
+        // also piggy-backed in these "local" statistics. It's obvious, really,
199
+        // if one carefully reads the *code* (but not the docs) in
200
+        // UI/VideoLayout/VideoLayout.js#updateLocalConnectionStats in
201
+        // jitsi-meet
202
+        // TODO: We should keep track of the remote resolution in _remoteStats,
203
+        // and notify about changes via separate events.
204
+        conference.on(
205
+            ConferenceEvents.CONNECTION_STATS,
206
+            this._updateLocalStats.bind(this));
207
+
208
+        // Save the last time we were unmuted.
209
+        conference.on(
210
+            ConferenceEvents.TRACK_MUTE_CHANGED,
211
+            (track) => {
212
+                if (track.isVideoTrack()) {
213
+                    if (track.isMuted()) {
214
+                        this._timeVideoUnmuted = -1;
215
+                    } else {
216
+                        this._maybeUpdateUnmuteTime();
217
+                    }
218
+                }
219
+            });
220
+        conference.on(
221
+            ConferenceEvents.TRACK_ADDED,
222
+            (track) => {
223
+                if (track.isVideoTrack() && !track.isMuted())
224
+                {
225
+                    this._maybeUpdateUnmuteTime();
226
+                }
227
+            });
228
+    }
229
+
230
+    /**
231
+     * Sets _timeVideoUnmuted if it was previously unset. If it was already set,
232
+     * doesn't change it.
233
+     */
234
+    _maybeUpdateUnmuteTime() {
235
+        if (this._timeVideoUnmuted < 0) {
236
+            this._timeVideoUnmuted = window.performance.now();
237
+        }
238
+    }
239
+
240
+    /**
241
+     * Calculates a new "connection quality" value.
242
+     * @param videoType {VideoType} the type of the video source (camera or
243
+     * a screen capture).
244
+     * @param isMuted {boolean} whether the local video is muted.
245
+     * @param resolutionName {Resolution} the input resolution used by the
246
+     * camera.
247
+     * @returns {*} the newly calculated connection quality.
248
+     */
249
+    _calculateConnectionQuality(videoType, isMuted, resolutionName) {
250
+
251
+        // resolutionName is an index into Resolutions (where "720" is
252
+        // "1280x720" and "960" is "960x720" ...).
253
+        let resolution = Resolutions[resolutionName];
254
+
255
+        let quality = 100;
256
+        let packetLoss;
257
+        // TODO: take into account packet loss for received streams
258
+        if (this._localStats.packetLoss) {
259
+            packetLoss = this._localStats.packetLoss.upload;
260
+
261
+            // Ugly Hack Alert (UHA):
262
+            // The packet loss for the upload direction is calculated based on
263
+            // incoming RTCP Receiver Reports. Since we don't have RTCP
264
+            // termination for audio, these reports come from the actual
265
+            // receivers in the conference and therefore the reported packet
266
+            // loss includes loss from the bridge to the receiver.
267
+            // When we are sending video this effect is small, because the
268
+            // number of video packets is much larger than the number of audio
269
+            // packets (and our calculation is based on the total number of
270
+            // received and lost packets).
271
+            // When video is muted, however, the effect might be significant,
272
+            // but we don't know what it is. We do know that it is positive, so
273
+            // as a temporary solution, until RTCP termination is implemented
274
+            // for the audio streams, we relax the packet loss checks here.
275
+            if (isMuted) {
276
+                packetLoss *= 0.5;
277
+            }
278
+        }
279
+
280
+        if (isMuted || !resolution || videoType === VideoType.DESKTOP
281
+            || this._timeIceConnected < 0
282
+            || this._timeVideoUnmuted < 0) {
283
+
284
+            // Calculate a value based on packet loss only.
285
+            if (packetLoss === undefined) {
286
+                logger.error("Cannot calculate connection quality, unknown "
287
+                    + "packet loss.");
288
+                quality = 100;
289
+            } else if (packetLoss <= 2) {
290
+                quality = 100; // Full 5 bars.
291
+            } else if (packetLoss <= 4) {
292
+                quality = 70; // 4 bars
293
+            } else if (packetLoss <= 6) {
294
+                quality = 50; // 3 bars
295
+            } else if (packetLoss <= 8) {
296
+                quality = 30; // 2 bars
297
+            } else if (packetLoss <= 12) {
298
+                quality = 10; // 1 bars
299
+            } else {
300
+                quality = 0; // Still 1 bar, but slower climb-up.
301
+            }
302
+        } else {
303
+            // Calculate a value based on the sending bitrate.
304
+
305
+            // time since sending of video was enabled.
306
+            let millisSinceStart = window.performance.now()
307
+                    - Math.max(this._timeVideoUnmuted, this._timeIceConnected);
308
+
309
+            // expected sending bitrate in perfect conditions
310
+            let target
311
+                = getTarget(this._simulcast, resolution, millisSinceStart);
312
+            target = 0.9 * target;
313
+
314
+            quality = 100 * this._localStats.bitrate.upload / target;
315
+
316
+            // Whatever the bitrate, drop early if there is significant loss
317
+            if (packetLoss && packetLoss >= 10) {
318
+                quality = Math.min(quality, 30);
319
+            }
320
+        }
321
+
322
+        // Make sure that the quality doesn't climb quickly
323
+        if (this._lastConnectionQualityUpdate > 0)
324
+        {
325
+            let maxIncreasePerSecond = 2;
326
+            let prevConnectionQuality = this._localStats.connectionQuality;
327
+            let diffSeconds
328
+                = (window.performance.now()
329
+                    - this._lastConnectionQualityUpdate) / 1000;
330
+            quality = Math.min(
331
+                quality,
332
+                prevConnectionQuality + diffSeconds * maxIncreasePerSecond);
333
+        }
334
+
335
+        return Math.min(100, quality);
336
+    }
337
+
338
+    /**
339
+     * Updates the localConnectionQuality value
340
+     * @param values {number} the new value. Should be in [0, 100].
341
+     */
342
+    _updateLocalConnectionQuality(value) {
343
+        this._localStats.connectionQuality = value;
344
+        this._lastConnectionQualityUpdate = window.performance.now();
345
+    }
346
+
347
+    /**
348
+     * Broadcasts the local statistics to all other participants in the
349
+     * conference.
350
+     */
351
+    _broadcastLocalStats() {
352
+        // Send only the data that remote participants care about.
353
+        let data = {
354
+            bitrate: this._localStats.bitrate,
355
+            packetLoss: this._localStats.packetLoss,
356
+            connectionQuality: this._localStats.connectionQuality
357
+        };
358
+
359
+        // TODO: It looks like the remote participants don't really "care"
360
+        // about the resolution, and they look at their local rendered
361
+        // resolution instead. Consider removing this.
362
+        let localVideoTrack
363
+            = this._conference.getLocalTracks(MediaType.VIDEO)
364
+                .find(track => track.isVideoTrack());
365
+        if (localVideoTrack && localVideoTrack.resolution) {
366
+            data.resolution = localVideoTrack.resolution;
367
+        }
368
+
369
+        try {
370
+            this._conference.broadcastEndpointMessage({
371
+                type: STATS_MESSAGE_TYPE,
372
+                values: data });
373
+        } catch (e) {
374
+            // We often hit this in the beginning of a call, before the data
375
+            // channel is ready. It is not a big problem, because we will
376
+            // send the statistics again after a few seconds, and the error is
377
+            // already logged elsewhere. So just ignore it.
378
+
379
+            //let errorMsg = "Failed to broadcast local stats";
380
+            //logger.error(errorMsg, e);
381
+            //GlobalOnErrorHandler.callErrorHandler(
382
+            //    new Error(errorMsg + ": " + e));
383
+        }
384
+    }
385
+
386
+    /**
387
+     * Updates the local statistics
388
+     * @param data new statistics
389
+     */
390
+    _updateLocalStats(data) {
391
+        let key;
392
+        let updateLocalConnectionQuality
393
+            = !this._conference.isConnectionInterrupted();
394
+        let localVideoTrack =
395
+                this._conference.getLocalTracks(MediaType.VIDEO)
396
+                    .find(track => track.isVideoTrack());
397
+        let videoType = localVideoTrack ? localVideoTrack.videoType : undefined;
398
+        let isMuted = localVideoTrack ? localVideoTrack.isMuted() : true;
399
+        let resolution = localVideoTrack ? localVideoTrack.resolution : null;
400
+
401
+        if (!isMuted) {
402
+            this._maybeUpdateUnmuteTime();
403
+        }
404
+
405
+        // Copy the fields already in 'data'.
406
+        for (key in data) {
407
+            if (data.hasOwnProperty(key)) {
408
+                this._localStats[key] = data[key];
409
+            }
410
+        }
411
+
412
+        // And re-calculate the connectionQuality field.
413
+        if (updateLocalConnectionQuality) {
414
+            this._updateLocalConnectionQuality(
415
+                this._calculateConnectionQuality(
416
+                    videoType,
417
+                    isMuted,
418
+                    resolution));
419
+        }
420
+
421
+        this.eventEmitter.emit(
422
+            ConnectionQualityEvents.LOCAL_STATS_UPDATED,
423
+            this._localStats);
424
+        this._broadcastLocalStats();
425
+    }
426
+
427
+    /**
428
+     * Updates remote statistics
429
+     * @param id the id of the remote participant
430
+     * @param data the statistics received
431
+     */
432
+    _updateRemoteStats(id, data) {
433
+            // Use only the fields we need
434
+            this._remoteStats[id] = {
435
+                bitrate: data.bitrate,
436
+                packetLoss: data.packetLoss,
437
+                connectionQuality: data.connectionQuality
438
+            };
439
+
440
+            this.eventEmitter.emit(
441
+                ConnectionQualityEvents.REMOTE_STATS_UPDATED,
442
+                id,
443
+                this._remoteStats[id]);
444
+    }
445
+
446
+    /**
447
+     * Returns the local statistics.
448
+     * Exported only for use in jitsi-meet-torture.
449
+     */
450
+    getStats() {
451
+        return this._localStats;
452
+    }
453
+}

+ 416
- 0
modules/connectivity/ParticipantConnectionStatus.js View File

@@ -0,0 +1,416 @@
1
+/* global __filename, module, require */
2
+var logger = require('jitsi-meet-logger').getLogger(__filename);
3
+var MediaType = require('../../service/RTC/MediaType');
4
+var RTCBrowserType = require('../RTC/RTCBrowserType');
5
+var RTCEvents = require('../../service/RTC/RTCEvents');
6
+
7
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
8
+import * as JitsiTrackEvents from '../../JitsiTrackEvents';
9
+import Statistics from '../statistics/statistics';
10
+
11
+/**
12
+ * Default value of 2000 milliseconds for
13
+ * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
14
+ *
15
+ * @type {number}
16
+ */
17
+const DEFAULT_RTC_MUTE_TIMEOUT = 2000;
18
+
19
+/**
20
+ * Class is responsible for emitting
21
+ * JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED events.
22
+ */
23
+export default class ParticipantConnectionStatus {
24
+    /**
25
+     * Creates new instance of <tt>ParticipantConnectionStatus</tt>.
26
+     *
27
+     * @constructor
28
+     * @param {RTC} rtc the RTC service instance
29
+     * @param {JitsiConference} conference parent conference instance
30
+     * @param {number} rtcMuteTimeout (optional) custom value for
31
+     * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
32
+     */
33
+    constructor(rtc, conference, rtcMuteTimeout) {
34
+        this.rtc = rtc;
35
+        this.conference = conference;
36
+        /**
37
+         * A map of the "endpoint ID"(which corresponds to the resource part
38
+         * of MUC JID(nickname)) to the timeout callback IDs scheduled using
39
+         * window.setTimeout.
40
+         * @type {Object.<string, number>}
41
+         */
42
+        this.trackTimers = {};
43
+        /**
44
+         * This map holds the endpoint connection status received from the JVB
45
+         * (as it might be different than the one stored in JitsiParticipant).
46
+         * Required for getting back in sync when remote video track is removed.
47
+         * @type {Object.<string, boolean>}
48
+         */
49
+        this.rtcConnStatusCache = { };
50
+        /**
51
+         * How long we're going to wait after the RTC video track muted event
52
+         * for the corresponding signalling mute event, before the connection
53
+         * interrupted is fired. The default value is
54
+         * {@link DEFAULT_RTC_MUTE_TIMEOUT}.
55
+         *
56
+         * @type {number} amount of time in milliseconds
57
+         */
58
+        this.rtcMuteTimeout
59
+            = typeof rtcMuteTimeout === 'number'
60
+                ? rtcMuteTimeout : DEFAULT_RTC_MUTE_TIMEOUT;
61
+        logger.info("RtcMuteTimeout set to: " + this.rtcMuteTimeout);
62
+    }
63
+
64
+    /**
65
+     * Initializes <tt>ParticipantConnectionStatus</tt> and bind required event
66
+     * listeners.
67
+     */
68
+    init() {
69
+
70
+        this._onEndpointConnStatusChanged
71
+            = this.onEndpointConnStatusChanged.bind(this);
72
+
73
+        this.rtc.addListener(
74
+            RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
75
+            this._onEndpointConnStatusChanged);
76
+
77
+        // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
78
+        // events for video type tracks when they stop receiving data which is
79
+        // often a sign that remote user is having connectivity issues
80
+        if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
81
+
82
+            this._onTrackRtcMuted = this.onTrackRtcMuted.bind(this);
83
+            this.rtc.addListener(
84
+                RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);
85
+
86
+            this._onTrackRtcUnmuted = this.onTrackRtcUnmuted.bind(this);
87
+            this.rtc.addListener(
88
+                RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);
89
+
90
+            // Track added/removed listeners are used to bind "mute"/"unmute"
91
+            // event handlers
92
+            this._onRemoteTrackAdded = this.onRemoteTrackAdded.bind(this);
93
+            this.conference.on(
94
+                JitsiConferenceEvents.TRACK_ADDED,
95
+                this._onRemoteTrackAdded);
96
+
97
+            this._onRemoteTrackRemoved = this.onRemoteTrackRemoved.bind(this);
98
+            this.conference.on(
99
+                JitsiConferenceEvents.TRACK_REMOVED,
100
+                this._onRemoteTrackRemoved);
101
+
102
+            // Listened which will be bound to JitsiRemoteTrack to listen for
103
+            // signalling mute/unmute events.
104
+            this._onSignallingMuteChanged
105
+                = this.onSignallingMuteChanged.bind(this);
106
+        }
107
+    }
108
+
109
+    /**
110
+     * Removes all event listeners and disposes of all resources held by this
111
+     * instance.
112
+     */
113
+    dispose() {
114
+
115
+        this.rtc.removeListener(
116
+            RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
117
+            this._onEndpointConnStatusChanged);
118
+
119
+        if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
120
+            this.rtc.removeListener(
121
+                RTCEvents.REMOTE_TRACK_MUTE,
122
+                this._onTrackRtcMuted);
123
+            this.rtc.removeListener(
124
+                RTCEvents.REMOTE_TRACK_UNMUTE,
125
+                this._onTrackRtcUnmuted);
126
+
127
+            this.conference.off(
128
+                JitsiConferenceEvents.TRACK_ADDED,
129
+                this._onRemoteTrackAdded);
130
+            this.conference.off(
131
+                JitsiConferenceEvents.TRACK_REMOVED,
132
+                this._onRemoteTrackRemoved);
133
+        }
134
+
135
+        Object.keys(this.trackTimers).forEach(function (participantId) {
136
+            this.clearTimeout(participantId);
137
+        }.bind(this));
138
+
139
+        // Clear RTC connection status cache
140
+        this.rtcConnStatusCache = {};
141
+    }
142
+
143
+    /**
144
+     * Handles RTCEvents.ENDPOINT_CONN_STATUS_CHANGED triggered when we receive
145
+     * notification over the data channel from the bridge about endpoint's
146
+     * connection status update.
147
+     * @param endpointId {string} the endpoint ID(MUC nickname/resource JID)
148
+     * @param isActive {boolean} true if the connection is OK or false otherwise
149
+     */
150
+    onEndpointConnStatusChanged(endpointId, isActive) {
151
+
152
+        logger.debug(
153
+            'Detector RTCEvents.ENDPOINT_CONN_STATUS_CHANGED('
154
+                + Date.now() +'): ' + endpointId + ': ' + isActive);
155
+
156
+        // Filter out events for the local JID for now
157
+        if (endpointId !== this.conference.myUserId()) {
158
+
159
+            // Cache the status received received over the data channels, as
160
+            // it will be needed to verify for out of sync when the remote video
161
+            // track is being removed.
162
+            this.rtcConnStatusCache[endpointId] = isActive;
163
+
164
+            var participant = this.conference.getParticipantById(endpointId);
165
+            // Delay the 'active' event until the video track gets
166
+            // the RTC unmuted event
167
+            if (isActive
168
+                    && RTCBrowserType.isVideoMuteOnConnInterruptedSupported()
169
+                    && participant
170
+                    && participant.hasAnyVideoTrackWebRTCMuted()
171
+                    && !participant.isVideoMuted()) {
172
+                logger.debug(
173
+                    'Ignoring RTCEvents.ENDPOINT_CONN_STATUS_CHANGED -'
174
+                        + ' will wait for unmute event');
175
+            } else {
176
+                this._changeConnectionStatus(endpointId, isActive);
177
+            }
178
+        }
179
+    }
180
+
181
+    _changeConnectionStatus(endpointId, newStatus) {
182
+        var participant = this.conference.getParticipantById(endpointId);
183
+        if (!participant) {
184
+            // This will happen when participant exits the conference with
185
+            // broken ICE connection and we join after that. The bridge keeps
186
+            // sending that notification until the conference does not expire.
187
+            logger.warn(
188
+                'Missed participant connection status update - ' +
189
+                    'no participant for endpoint: ' + endpointId);
190
+            return;
191
+        }
192
+        if (participant.isConnectionActive() !== newStatus) {
193
+
194
+            participant._setIsConnectionActive(newStatus);
195
+
196
+            logger.debug(
197
+                'Emit endpoint conn status(' + Date.now() + ') '
198
+                    + endpointId + ": " + newStatus);
199
+
200
+            // Log the event on CallStats
201
+            Statistics.sendLog(
202
+                JSON.stringify({
203
+                    id: 'peer.conn.status',
204
+                    participant: endpointId,
205
+                    status: newStatus
206
+                }));
207
+
208
+            // and analytics
209
+            Statistics.analytics.sendEvent('peer.conn.status',
210
+                {label: newStatus});
211
+
212
+            this.conference.eventEmitter.emit(
213
+                JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
214
+                endpointId, newStatus);
215
+        }
216
+    }
217
+
218
+    /**
219
+     * Reset the postponed "connection interrupted" event which was previously
220
+     * scheduled as a timeout on RTC 'onmute' event.
221
+     *
222
+     * @param participantId the participant for which the "connection
223
+     * interrupted" timeout was scheduled
224
+     */
225
+    clearTimeout(participantId) {
226
+        if (this.trackTimers[participantId]) {
227
+            window.clearTimeout(this.trackTimers[participantId]);
228
+            this.trackTimers[participantId] = null;
229
+        }
230
+    }
231
+
232
+    /**
233
+     * Bind signalling mute event listeners for video {JitsiRemoteTrack} when
234
+     * a new one is added to the conference.
235
+     *
236
+     * @param {JitsiTrack} remoteTrack the {JitsiTrack} which is being added to
237
+     * the conference.
238
+     */
239
+    onRemoteTrackAdded(remoteTrack) {
240
+        if (!remoteTrack.isLocal()
241
+                && remoteTrack.getType() === MediaType.VIDEO) {
242
+
243
+            logger.debug(
244
+                'Detector on remote track added for: '
245
+                    + remoteTrack.getParticipantId());
246
+
247
+            remoteTrack.on(
248
+                JitsiTrackEvents.TRACK_MUTE_CHANGED,
249
+                this._onSignallingMuteChanged);
250
+        }
251
+    }
252
+
253
+    /**
254
+     * Removes all event listeners bound to the remote video track and clears
255
+     * any related timeouts.
256
+     *
257
+     * @param {JitsiRemoteTrack} remoteTrack the remote track which is being
258
+     * removed from the conference.
259
+     */
260
+    onRemoteTrackRemoved(remoteTrack) {
261
+        if (!remoteTrack.isLocal()
262
+                && remoteTrack.getType() === MediaType.VIDEO) {
263
+
264
+            const endpointId = remoteTrack.getParticipantId();
265
+
266
+            logger.debug(
267
+                'Detector on remote track removed: ' + endpointId);
268
+
269
+            remoteTrack.off(
270
+                JitsiTrackEvents.TRACK_MUTE_CHANGED,
271
+                this._onSignallingMuteChanged);
272
+
273
+            this.clearTimeout(endpointId);
274
+
275
+            // Only if we're using video muted events - check if the JVB status
276
+            // should be restored from cache.
277
+            if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported())
278
+            {
279
+                this.maybeRestoreCachedStatus(endpointId);
280
+            }
281
+        }
282
+    }
283
+
284
+    /**
285
+     * When RTC video track muted events are taken into account,
286
+     * at the point when the track is being removed we have to update
287
+     * to the current connectivity status according to the JVB. That's
288
+     * because if the current track is muted then the new one which
289
+     * replaces it is always added as unmuted and there may be no
290
+     * 'muted'/'unmuted' event sequence if the connection restores in
291
+     * the meantime.
292
+     *
293
+     * XXX See onEndpointConnStatusChanged method where the update is
294
+     * postponed and which is the cause for this workaround. If we
295
+     * decide to not wait for video unmuted event and accept the JVB
296
+     * status immediately then it's fine to remove the code below.
297
+     */
298
+    maybeRestoreCachedStatus(endpointId) {
299
+        var participant = this.conference.getParticipantById(endpointId);
300
+        if (!participant) {
301
+            // Probably the participant is no longer in the conference
302
+            // (at the time of writing this code, participant is
303
+            // detached from the conference and TRACK_REMOVED events are
304
+            // fired),
305
+            // so we don't care, but let's print the warning for
306
+            // debugging purpose
307
+            logger.warn(
308
+                'maybeRestoreCachedStatus - ' +
309
+                'no participant for endpoint: ' + endpointId);
310
+            return;
311
+        }
312
+
313
+        const isConnectionActive = participant.isConnectionActive();
314
+        const hasAnyVideoRTCMuted = participant.hasAnyVideoTrackWebRTCMuted();
315
+        let isConnActiveByJvb = this.rtcConnStatusCache[endpointId];
316
+
317
+        // If no status was received from the JVB it means that it's active
318
+        // (the bridge does not send notification unless there is a problem).
319
+        if (typeof isConnActiveByJvb !== 'boolean') {
320
+            logger.debug("Assuming connection active by JVB - no notification");
321
+            isConnActiveByJvb = true;
322
+        }
323
+
324
+        logger.debug(
325
+            "Remote track removed, is active: " + isConnectionActive
326
+            + " is active(jvb):" + isConnActiveByJvb
327
+            + " video RTC muted:" + hasAnyVideoRTCMuted);
328
+
329
+        if (!isConnectionActive && isConnActiveByJvb && !hasAnyVideoRTCMuted) {
330
+            // FIXME adjust the log level or remove the message completely once
331
+            // the feature gets mature enough.
332
+            logger.info(
333
+                "Remote track removed for disconnected" +
334
+                " participant, when the status according to" +
335
+                " the JVB is connected. Adjusting to the JVB value.");
336
+            this._changeConnectionStatus(endpointId, isConnActiveByJvb);
337
+        }
338
+    }
339
+
340
+    /**
341
+     * Handles RTC 'onmute' event for the video track.
342
+     *
343
+     * @param {JitsiRemoteTrack} track the video track for which 'onmute' event
344
+     * will be processed.
345
+     */
346
+    onTrackRtcMuted(track) {
347
+        var participantId = track.getParticipantId();
348
+        var participant = this.conference.getParticipantById(participantId);
349
+        logger.debug('Detector track RTC muted: ' + participantId);
350
+        if (!participant) {
351
+            logger.error('No participant for id: ' + participantId);
352
+            return;
353
+        }
354
+        if (!participant.isVideoMuted()) {
355
+            // If the user is not muted according to the signalling we'll give
356
+            // it some time, before the connection interrupted event is
357
+            // triggered.
358
+            this.trackTimers[participantId] = window.setTimeout(function () {
359
+                if (!track.isMuted() && participant.isConnectionActive()) {
360
+                    logger.info(
361
+                        'Connection interrupted through the RTC mute: '
362
+                            + participantId, Date.now());
363
+                    this._changeConnectionStatus(participantId, false);
364
+                }
365
+                this.clearTimeout(participantId);
366
+            }.bind(this), this.rtcMuteTimeout);
367
+        }
368
+    }
369
+
370
+    /**
371
+     * Handles RTC 'onunmute' event for the video track.
372
+     *
373
+     * @param {JitsiRemoteTrack} track the video track for which 'onunmute'
374
+     * event will be processed.
375
+     */
376
+    onTrackRtcUnmuted(track) {
377
+        logger.debug('Detector track RTC unmuted: ', track);
378
+        var participantId = track.getParticipantId();
379
+        if (!track.isMuted() &&
380
+            !this.conference.getParticipantById(participantId)
381
+                .isConnectionActive()) {
382
+            logger.info(
383
+                'Detector connection restored through the RTC unmute: '
384
+                    + participantId, Date.now());
385
+            this._changeConnectionStatus(participantId, true);
386
+        }
387
+        this.clearTimeout(participantId);
388
+    }
389
+
390
+    /**
391
+     * Here the signalling "mute"/"unmute" events are processed.
392
+     *
393
+     * @param {JitsiRemoteTrack} track the remote video track for which
394
+     * the signalling mute/unmute event will be processed.
395
+     */
396
+    onSignallingMuteChanged (track) {
397
+        logger.debug(
398
+            'Detector on track signalling mute changed: ',
399
+            track, track.isMuted());
400
+        var isMuted = track.isMuted();
401
+        var participantId = track.getParticipantId();
402
+        var participant = this.conference.getParticipantById(participantId);
403
+        if (!participant) {
404
+            logger.error('No participant for id: ' + participantId);
405
+            return;
406
+        }
407
+        var isConnectionActive = participant.isConnectionActive();
408
+        if (isMuted && isConnectionActive && this.trackTimers[participantId]) {
409
+            logger.debug(
410
+                'Signalling got in sync - cancelling task for: '
411
+                    + participantId);
412
+            this.clearTimeout(participantId);
413
+        }
414
+    }
415
+
416
+}

+ 100
- 0
modules/statistics/AnalyticsAdapter.js View File

@@ -0,0 +1,100 @@
1
+var RTCBrowserType = require("../RTC/RTCBrowserType");
2
+
3
+function NoopAnalytics() {}
4
+NoopAnalytics.prototype.sendEvent = function () {};
5
+
6
+function AnalyticsAdapter() {
7
+    this.browserName = RTCBrowserType.getBrowserName();
8
+    /**
9
+     * Map of properties that will be added to every event
10
+     */
11
+    this.permanentProperties = {};
12
+}
13
+
14
+// some events may happen before init or implementation script download
15
+// in this case we accumulate them in this array and send them on init
16
+AnalyticsAdapter.eventsQueue = [];
17
+
18
+/**
19
+ * Sends analytics event.
20
+ * @param {String} action the name of the event
21
+ * @param {Object} data can be any JSON object
22
+ */
23
+AnalyticsAdapter.prototype.sendEvent = function (action, data = {}) {
24
+    if(this._checkAnalyticsAndMaybeCacheEvent(action, data)) {
25
+        data.browserName = this.browserName;
26
+        try {
27
+            this.analytics.sendEvent(action,
28
+                Object.assign({}, this.permanentProperties, data));
29
+        } catch (ignored) { // eslint-disable-line no-empty
30
+        }
31
+    }
32
+};
33
+
34
+/**
35
+ * Since we asynchronously load the integration of the analytics API and the
36
+ * analytics API may asynchronously load its implementation (e.g. Google
37
+ * Analytics), we cannot make the decision with respect to which analytics
38
+ * implementation we will use here and we have to postpone it i.e. we will make
39
+ * a lazy decision, will wait for loaded or dispose methods to be called.
40
+ * in the meantime we accumulate any events received. We should call this
41
+ * method before trying to send the event.
42
+ * @param action
43
+ * @param data
44
+ */
45
+AnalyticsAdapter.prototype._checkAnalyticsAndMaybeCacheEvent
46
+= function (action, data) {
47
+    if (this.analytics === null || typeof this.analytics === 'undefined') {
48
+        // missing this.analytics but have window implementation, let's use it
49
+        if (window.Analytics) {
50
+            this.loaded();
51
+        }
52
+        else {
53
+            AnalyticsAdapter.eventsQueue.push({
54
+                action: action,
55
+                data: data
56
+            });
57
+            // stored, lets break here
58
+            return false;
59
+        }
60
+    }
61
+    return true;
62
+};
63
+
64
+
65
+/**
66
+ * Dispose analytics. Clears any available queue element and sets
67
+ * NoopAnalytics to be used.
68
+ */
69
+AnalyticsAdapter.prototype.dispose = function () {
70
+    this.analytics = new NoopAnalytics();
71
+    AnalyticsAdapter.eventsQueue.length = 0;
72
+};
73
+
74
+/**
75
+ * Adds map of properties that will be added to every event.
76
+ * @param {Object} properties the map of properties
77
+ */
78
+AnalyticsAdapter.prototype.addPermanentProperties = function (properties) {
79
+    this.permanentProperties
80
+        = Object.assign(this.permanentProperties, properties);
81
+};
82
+
83
+/**
84
+ * Loaded analytics script. Sens queued events.
85
+ */
86
+AnalyticsAdapter.prototype.loaded = function () {
87
+    var AnalyticsImpl = window.Analytics || NoopAnalytics;
88
+
89
+    this.analytics = new AnalyticsImpl();
90
+
91
+    // new analytics lets send all events if any
92
+    if (AnalyticsAdapter.eventsQueue.length) {
93
+        AnalyticsAdapter.eventsQueue.forEach(function (event) {
94
+            this.sendEvent(event.action, event.data);
95
+        }.bind(this));
96
+        AnalyticsAdapter.eventsQueue.length = 0;
97
+    }
98
+};
99
+
100
+module.exports = new AnalyticsAdapter();

+ 114
- 69
modules/statistics/CallStats.js View File

@@ -47,25 +47,40 @@ var fabricEvent = {
47 47
 
48 48
 var callStats = null;
49 49
 
50
+/**
51
+ * The user id to report to callstats as destination.
52
+ * @type {string}
53
+ */
54
+const DEFAULT_REMOTE_USER = "jitsi";
55
+
50 56
 function initCallback (err, msg) {
51 57
     logger.log("CallStats Status: err=" + err + " msg=" + msg);
52 58
 
59
+    CallStats.initializeInProgress = false;
60
+
53 61
     // there is no lib, nothing to report to
54
-    if (err !== 'success')
62
+    if (err !== 'success') {
63
+        CallStats.initializeFailed = true;
55 64
         return;
56
-
57
-    CallStats.initialized = true;
65
+    }
58 66
 
59 67
     var ret = callStats.addNewFabric(this.peerconnection,
60
-        Strophe.getResourceFromJid(this.session.peerjid),
68
+        DEFAULT_REMOTE_USER,
61 69
         callStats.fabricUsage.multiplex,
62 70
         this.confID,
63 71
         this.pcCallback.bind(this));
64 72
 
65 73
     var fabricInitialized = (ret.status === 'success');
66 74
 
67
-    if(!fabricInitialized)
68
-        console.log("callstats fabric not initilized", ret.message);
75
+    if(!fabricInitialized) {
76
+        CallStats.initializeFailed = true;
77
+        logger.log("callstats fabric not initilized", ret.message);
78
+        return;
79
+    }
80
+
81
+    CallStats.initializeFailed = false;
82
+    CallStats.initialized = true;
83
+    CallStats.feedbackEnabled = true;
69 84
 
70 85
     // notify callstats about failures if there were any
71 86
     if (CallStats.reportsQueue.length) {
@@ -129,31 +144,33 @@ function _try_catch (f) {
129 144
  */
130 145
 var CallStats = _try_catch(function(jingleSession, Settings, options) {
131 146
     try{
132
-        //check weather that should work with more than 1 peerconnection
133
-        if(!callStats) {
134
-            callStats = new callstats($, io, jsSHA);
135
-        } else {
136
-            return;
137
-        }
147
+        CallStats.feedbackEnabled = false;
148
+        callStats = new callstats($, io, jsSHA); // eslint-disable-line new-cap
138 149
 
139
-        this.session = jingleSession;
140 150
         this.peerconnection = jingleSession.peerconnection.peerconnection;
141 151
 
142
-        this.userID = Settings.getCallStatsUserName();
143
-
152
+        this.userID = {
153
+            aliasName: Strophe.getResourceFromJid(jingleSession.room.myroomjid),
154
+            userName: Settings.getCallStatsUserName()
155
+        };
156
+        
144 157
         // The confID is case sensitive!!!
145 158
         this.confID = options.callStatsConfIDNamespace + "/" + options.roomName;
146 159
 
160
+        this.callStatsID = options.callStatsID;
161
+        this.callStatsSecret = options.callStatsSecret;
162
+
163
+        CallStats.initializeInProgress = true;
147 164
         //userID is generated or given by the origin server
148
-        callStats.initialize(options.callStatsID,
149
-            options.callStatsSecret,
165
+        callStats.initialize(this.callStatsID,
166
+            this.callStatsSecret,
150 167
             this.userID,
151 168
             initCallback.bind(this));
152 169
 
153 170
     } catch (e) {
154
-        // The callstats.io API failed to initialize (e.g. because its
155
-        // download failed to succeed in general or on time). Further
156
-        // attempts to utilize it cannot possibly succeed.
171
+        // The callstats.io API failed to initialize (e.g. because its download
172
+        // did not succeed in general or on time). Further attempts to utilize
173
+        // it cannot possibly succeed.
157 174
         GlobalOnErrorHandler.callErrorHandler(e);
158 175
         callStats = null;
159 176
         logger.error(e);
@@ -172,6 +189,43 @@ CallStats.reportsQueue = [];
172 189
  */
173 190
 CallStats.initialized = false;
174 191
 
192
+/**
193
+ * Whether we are in progress of initializing.
194
+ * @type {boolean}
195
+ */
196
+CallStats.initializeInProgress = false;
197
+
198
+/**
199
+ * Whether we tried to initialize and it failed.
200
+ * @type {boolean}
201
+ */
202
+CallStats.initializeFailed = false;
203
+
204
+/**
205
+ * Shows weather sending feedback is enabled or not
206
+ * @type {boolean}
207
+ */
208
+CallStats.feedbackEnabled = false;
209
+
210
+/**
211
+ * Checks whether we need to re-initialize callstats and starts the process.
212
+ * @private
213
+ */
214
+CallStats._checkInitialize = function () {
215
+    if (CallStats.initialized || !CallStats.initializeFailed
216
+        || !callStats || CallStats.initializeInProgress)
217
+        return;
218
+
219
+    // callstats object created, not initialized and it had previously failed,
220
+    // and there is no init in progress, so lets try initialize it again
221
+    CallStats.initializeInProgress = true;
222
+    callStats.initialize(
223
+        callStats.callStatsID,
224
+        callStats.callStatsSecret,
225
+        callStats.userID,
226
+        initCallback.bind(callStats));
227
+};
228
+
175 229
 /**
176 230
  * Type of pending reports, can be event or an error.
177 231
  * @type {{ERROR: string, EVENT: string}}
@@ -183,10 +237,8 @@ var reportType = {
183 237
 };
184 238
 
185 239
 CallStats.prototype.pcCallback = _try_catch(function (err, msg) {
186
-    if (!callStats) {
187
-        return;
188
-    }
189
-    logger.log("Monitoring status: "+ err + " msg: " + msg);
240
+    if (callStats && err !== 'success')
241
+        logger.error("Monitoring status: "+ err + " msg: " + msg);
190 242
 });
191 243
 
192 244
 /**
@@ -206,11 +258,9 @@ function (ssrc, isLocal, usageLabel, containerId) {
206 258
     if(!callStats) {
207 259
         return;
208 260
     }
261
+
209 262
     // 'focus' is default remote user ID for now
210
-    var callStatsId = 'focus';
211
-    if (isLocal) {
212
-        callStatsId = this.userID;
213
-    }
263
+    const callStatsId = isLocal ? this.userID : 'focus';
214 264
 
215 265
     _try_catch(function() {
216 266
         logger.debug(
@@ -220,8 +270,7 @@ function (ssrc, isLocal, usageLabel, containerId) {
220 270
             this.confID,
221 271
             ssrc,
222 272
             usageLabel,
223
-            containerId
224
-        );
273
+            containerId);
225 274
         if(CallStats.initialized) {
226 275
             callStats.associateMstWithUserID(
227 276
                 this.peerconnection,
@@ -229,19 +278,19 @@ function (ssrc, isLocal, usageLabel, containerId) {
229 278
                 this.confID,
230 279
                 ssrc,
231 280
                 usageLabel,
232
-                containerId
233
-            );
281
+                containerId);
234 282
         }
235 283
         else {
236 284
             CallStats.reportsQueue.push({
237 285
                 type: reportType.MST_WITH_USERID,
238 286
                 data: {
239
-                    callStatsId: callStatsId,
240
-                    ssrc: ssrc,
241
-                    usageLabel: usageLabel,
242
-                    containerId: containerId
287
+                    callStatsId,
288
+                    containerId,
289
+                    ssrc,
290
+                    usageLabel
243 291
                 }
244 292
             });
293
+            CallStats._checkInitialize();
245 294
         }
246 295
     }).bind(this)();
247 296
 };
@@ -253,13 +302,12 @@ function (ssrc, isLocal, usageLabel, containerId) {
253 302
  * @param {CallStats} cs callstats instance related to the event
254 303
  */
255 304
 CallStats.sendMuteEvent = _try_catch(function (mute, type, cs) {
305
+    let event;
256 306
 
257
-    var event = null;
258 307
     if (type === "video") {
259
-        event = (mute? fabricEvent.videoPause : fabricEvent.videoResume);
260
-    }
261
-    else {
262
-        event = (mute? fabricEvent.audioMute : fabricEvent.audioUnmute);
308
+        event = mute ? fabricEvent.videoPause : fabricEvent.videoResume;
309
+    } else {
310
+        event = mute ? fabricEvent.audioMute : fabricEvent.audioUnmute;
263 311
     }
264 312
 
265 313
     CallStats._reportEvent.call(cs, event);
@@ -272,9 +320,9 @@ CallStats.sendMuteEvent = _try_catch(function (mute, type, cs) {
272 320
  * @param {CallStats} cs callstats instance related to the event
273 321
  */
274 322
 CallStats.sendScreenSharingEvent = _try_catch(function (start, cs) {
275
-
276
-    CallStats._reportEvent.call(cs,
277
-        start? fabricEvent.screenShareStart : fabricEvent.screenShareStop);
323
+    CallStats._reportEvent.call(
324
+        cs,
325
+        start ? fabricEvent.screenShareStart : fabricEvent.screenShareStop);
278 326
 });
279 327
 
280 328
 /**
@@ -282,9 +330,7 @@ CallStats.sendScreenSharingEvent = _try_catch(function (start, cs) {
282 330
  * @param {CallStats} cs callstats instance related to the event
283 331
  */
284 332
 CallStats.sendDominantSpeakerEvent = _try_catch(function (cs) {
285
-
286
-    CallStats._reportEvent.call(cs,
287
-        fabricEvent.dominantSpeaker);
333
+    CallStats._reportEvent.call(cs, fabricEvent.dominantSpeaker);
288 334
 });
289 335
 
290 336
 /**
@@ -293,7 +339,6 @@ CallStats.sendDominantSpeakerEvent = _try_catch(function (cs) {
293 339
  * @param {CallStats} cs callstats instance related to the event
294 340
  */
295 341
 CallStats.sendActiveDeviceListEvent = _try_catch(function (devicesData, cs) {
296
-
297 342
     CallStats._reportEvent.call(cs, fabricEvent.activeDeviceList, devicesData);
298 343
 });
299 344
 
@@ -315,6 +360,7 @@ CallStats._reportEvent = function (event, eventData) {
315 360
                 type: reportType.EVENT,
316 361
                 data: {event: event, eventData: eventData}
317 362
             });
363
+        CallStats._checkInitialize();
318 364
     }
319 365
 };
320 366
 
@@ -329,18 +375,6 @@ CallStats.prototype.sendTerminateEvent = _try_catch(function () {
329 375
         callStats.fabricEvent.fabricTerminated, this.confID);
330 376
 });
331 377
 
332
-/**
333
- * Notifies CallStats that audio problems are detected.
334
- *
335
- * @param {Error} e error to send
336
- * @param {CallStats} cs callstats instance related to the error (optional)
337
- */
338
-CallStats.prototype.sendDetectedAudioProblem = _try_catch(function (e) {
339
-    CallStats._reportError.call(this, wrtcFuncNames.signalingError, e,
340
-        this.peerconnection);
341
-});
342
-
343
-
344 378
 /**
345 379
  * Notifies CallStats for ice connection failed
346 380
  * @param {RTCPeerConnection} pc connection on which failure occured.
@@ -360,16 +394,15 @@ CallStats.prototype.sendIceConnectionFailedEvent = _try_catch(function (pc, cs){
360 394
  */
361 395
 CallStats.prototype.sendFeedback = _try_catch(
362 396
 function(overallFeedback, detailedFeedback) {
363
-    if(!CallStats.initialized) {
397
+    if(!CallStats.feedbackEnabled) {
364 398
         return;
365 399
     }
366
-    var feedbackString =    '{"userID":"' + this.userID + '"' +
367
-                            ', "overall":' + overallFeedback +
368
-                            ', "comment": "' + detailedFeedback + '"}';
369 400
 
370
-    var feedbackJSON = JSON.parse(feedbackString);
371
-
372
-    callStats.sendUserFeedback(this.confID, feedbackJSON);
401
+    callStats.sendUserFeedback(this.confID, {
402
+        userID: this.userID,
403
+        overall: overallFeedback,
404
+        comment: detailedFeedback
405
+    });
373 406
 });
374 407
 
375 408
 /**
@@ -392,6 +425,7 @@ CallStats._reportError = function (type, e, pc) {
392 425
             type: reportType.ERROR,
393 426
             data: { type: type, error: e, pc: pc}
394 427
         });
428
+        CallStats._checkInitialize();
395 429
     }
396 430
     // else just ignore it
397 431
 };
@@ -468,8 +502,19 @@ CallStats.sendAddIceCandidateFailed = _try_catch(function (e, pc, cs) {
468 502
  * @param {CallStats} cs callstats instance related to the error (optional)
469 503
  */
470 504
 CallStats.sendApplicationLog = _try_catch(function (e, cs) {
471
-    CallStats._reportError
472
-        .call(cs, wrtcFuncNames.applicationLog, e, null);
505
+    CallStats._reportError.call(cs, wrtcFuncNames.applicationLog, e, null);
473 506
 });
474 507
 
508
+/**
509
+ * Clears allocated resources.
510
+ */
511
+CallStats.dispose = function () {
512
+    // The next line is commented because we need to be able to send feedback
513
+    // even after the conference has been destroyed.
514
+    // callStats = null;
515
+    CallStats.initialized = false;
516
+    CallStats.initializeFailed = false;
517
+    CallStats.initializeInProgress = false;
518
+};
519
+
475 520
 module.exports = CallStats;

+ 0
- 1
modules/statistics/LocalStatsCollector.js View File

@@ -1,4 +1,3 @@
1
-/* global config */
2 1
 /**
3 2
  * Provides statistics for the local stream.
4 3
  */

+ 141
- 165
modules/statistics/RTPStatsCollector.js View File

@@ -1,14 +1,14 @@
1 1
 /* global require */
2
-/* jshint -W101 */
3 2
 
3
+var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
4 4
 var logger = require("jitsi-meet-logger").getLogger(__filename);
5 5
 var RTCBrowserType = require("../RTC/RTCBrowserType");
6
-var StatisticsEvents = require("../../service/statistics/Events");
7
-var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
6
+import * as StatisticsEvents from "../../service/statistics/Events";
8 7
 
9 8
 /* Whether we support the browser we are running into for logging statistics */
10 9
 var browserSupported = RTCBrowserType.isChrome() ||
11
-        RTCBrowserType.isOpera() || RTCBrowserType.isFirefox();
10
+        RTCBrowserType.isOpera() || RTCBrowserType.isFirefox() ||
11
+        RTCBrowserType.isNWJS();
12 12
 
13 13
 /**
14 14
  * The LibJitsiMeet browser-agnostic names of the browser-specific keys reported
@@ -45,6 +45,8 @@ KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME] = {
45 45
 };
46 46
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_OPERA] =
47 47
     KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
48
+KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_NWJS] =
49
+    KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
48 50
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_IEXPLORER] =
49 51
     KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
50 52
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_SAFARI] =
@@ -65,10 +67,6 @@ function calculatePacketLoss(lostPackets, totalPackets) {
65 67
     return Math.round((lostPackets/totalPackets)*100);
66 68
 }
67 69
 
68
-function formatAudioLevel(audioLevel) {
69
-    return Math.min(Math.max(audioLevel, 0), 1);
70
-}
71
-
72 70
 /**
73 71
  * Checks whether a certain record should be included in the logged statistics.
74 72
  */
@@ -103,65 +101,51 @@ function acceptReport(id, type) {
103 101
 }
104 102
 
105 103
 /**
106
- * Peer statistics data holder.
104
+ * Holds "statistics" for a single SSRC.
107 105
  * @constructor
108 106
  */
109
-function PeerStats() {
110
-    this.ssrc2Loss = {};
111
-    this.ssrc2AudioLevel = {};
112
-    this.ssrc2bitrate = {
107
+function SsrcStats() {
108
+    this.loss = {};
109
+    this.bitrate = {
113 110
         download: 0,
114 111
         upload: 0
115 112
     };
116
-    this.ssrc2resolution = {};
113
+    this.resolution = {};
117 114
 }
118 115
 
119 116
 /**
120
- * Sets packets loss rate for given <tt>ssrc</tt> that belong to the peer
121
- * represented by this instance.
122
- * @param lossRate new packet loss rate value to be set.
117
+ * Sets the "loss" object.
118
+ * @param loss the value to set.
123 119
  */
124
-PeerStats.prototype.setSsrcLoss = function (lossRate) {
125
-    this.ssrc2Loss = lossRate || {};
120
+SsrcStats.prototype.setLoss = function (loss) {
121
+    this.loss = loss || {};
126 122
 };
127 123
 
128 124
 /**
129
- * Sets resolution that belong to the ssrc
130
- * represented by this instance.
125
+ * Sets resolution that belong to the ssrc represented by this instance.
131 126
  * @param resolution new resolution value to be set.
132 127
  */
133
-PeerStats.prototype.setSsrcResolution = function (resolution) {
134
-    this.ssrc2resolution = resolution || {};
128
+SsrcStats.prototype.setResolution = function (resolution) {
129
+    this.resolution = resolution || {};
135 130
 };
136 131
 
137 132
 /**
138
- * Sets the bit rate for given <tt>ssrc</tt> that belong to the peer
139
- * represented by this instance.
140
- * @param bitrate new bitrate value to be set.
133
+ * Adds the "download" and "upload" fields from the "bitrate" parameter to
134
+ * the respective fields of the "bitrate" field of this object.
135
+ * @param bitrate an object holding the values to add.
141 136
  */
142
-PeerStats.prototype.setSsrcBitrate = function (bitrate) {
143
-    this.ssrc2bitrate.download += bitrate.download;
144
-    this.ssrc2bitrate.upload += bitrate.upload;
137
+SsrcStats.prototype.addBitrate = function (bitrate) {
138
+    this.bitrate.download += bitrate.download;
139
+    this.bitrate.upload += bitrate.upload;
145 140
 };
146 141
 
147 142
 /**
148 143
  * Resets the bit rate for given <tt>ssrc</tt> that belong to the peer
149 144
  * represented by this instance.
150 145
  */
151
-PeerStats.prototype.resetSsrcBitrate = function () {
152
-    this.ssrc2bitrate.download = 0;
153
-    this.ssrc2bitrate.upload = 0;
154
-};
155
-
156
-/**
157
- * Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
158
- * the stream which belongs to the peer represented by this instance.
159
- * @param audioLevel the new audio level value to be set. Value is truncated to
160
- *        fit the range from 0 to 1.
161
- */
162
-PeerStats.prototype.setSsrcAudioLevel = function (audioLevel) {
163
-    // Range limit 0 - 1
164
-    this.ssrc2AudioLevel = formatAudioLevel(audioLevel);
146
+SsrcStats.prototype.resetBitrate = function () {
147
+    this.bitrate.download = 0;
148
+    this.bitrate.upload = 0;
165 149
 };
166 150
 
167 151
 function ConferenceStats() {
@@ -194,7 +178,7 @@ function ConferenceStats() {
194 178
 /**
195 179
  * <tt>StatsCollector</tt> registers for stats updates of given
196 180
  * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
197
- * stats are extracted and put in {@link PeerStats} objects. Once the processing
181
+ * stats are extracted and put in {@link SsrcStats} objects. Once the processing
198 182
  * is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
199 183
  * instance as an event source.
200 184
  *
@@ -202,16 +186,13 @@ function ConferenceStats() {
202 186
  * @param audioLevelsInterval
203 187
  * @param statsInterval stats refresh interval given in ms.
204 188
  * @param eventEmitter
205
- * @param config {object} supports the following properties: disableAudioLevels,
206
- * disableStats, logStats
207 189
  * @constructor
208 190
  */
209 191
 function StatsCollector(
210 192
         peerconnection,
211 193
         audioLevelsInterval,
212 194
         statsInterval,
213
-        eventEmitter,
214
-        config) {
195
+        eventEmitter) {
215 196
     // StatsCollector depends entirely on the format of the reports returned by
216 197
     // RTCPeerConnection#getStats. Given that the value of
217 198
     // RTCBrowserType#getBrowserType() is very unlikely to change at runtime, it
@@ -244,10 +225,9 @@ function StatsCollector(
244 225
     this.baselineAudioLevelsReport = null;
245 226
     this.currentAudioLevelsReport = null;
246 227
     this.currentStatsReport = null;
247
-    this.baselineStatsReport = null;
228
+    this.previousStatsReport = null;
248 229
     this.audioLevelsIntervalId = null;
249 230
     this.eventEmitter = eventEmitter;
250
-    this.config = config || {};
251 231
     this.conferenceStats = new ConferenceStats();
252 232
 
253 233
     /**
@@ -273,7 +253,7 @@ function StatsCollector(
273 253
 
274 254
     this.statsIntervalId = null;
275 255
     this.statsIntervalMilis = statsInterval;
276
-    // Map of ssrcs to PeerStats
256
+    // Map of ssrcs to SsrcStats
277 257
     this.ssrc2stats = {};
278 258
 }
279 259
 
@@ -312,33 +292,35 @@ StatsCollector.prototype.errorCallback = function (error) {
312 292
 /**
313 293
  * Starts stats updates.
314 294
  */
315
-StatsCollector.prototype.start = function () {
295
+StatsCollector.prototype.start = function (startAudioLevelStats) {
316 296
     var self = this;
317
-    this.audioLevelsIntervalId = setInterval(
318
-        function () {
319
-            // Interval updates
320
-            self.peerconnection.getStats(
321
-                function (report) {
322
-                    var results = null;
323
-                    if (!report || !report.result ||
324
-                        typeof report.result != 'function') {
325
-                        results = report;
326
-                    }
327
-                    else {
328
-                        results = report.result();
329
-                    }
330
-                    self.currentAudioLevelsReport = results;
331
-                    self.processAudioLevelReport();
332
-                    self.baselineAudioLevelsReport =
333
-                        self.currentAudioLevelsReport;
334
-                },
335
-                self.errorCallback
336
-            );
337
-        },
338
-        self.audioLevelsIntervalMilis
339
-    );
297
+    if(startAudioLevelStats) {
298
+        this.audioLevelsIntervalId = setInterval(
299
+            function () {
300
+                // Interval updates
301
+                self.peerconnection.getStats(
302
+                    function (report) {
303
+                        var results = null;
304
+                        if (!report || !report.result ||
305
+                            typeof report.result != 'function') {
306
+                            results = report;
307
+                        }
308
+                        else {
309
+                            results = report.result();
310
+                        }
311
+                        self.currentAudioLevelsReport = results;
312
+                        self.processAudioLevelReport();
313
+                        self.baselineAudioLevelsReport =
314
+                            self.currentAudioLevelsReport;
315
+                    },
316
+                    self.errorCallback
317
+                );
318
+            },
319
+            self.audioLevelsIntervalMilis
320
+        );
321
+    }
340 322
 
341
-    if (!this.config.disableStats && browserSupported) {
323
+    if (browserSupported) {
342 324
         this.statsIntervalId = setInterval(
343 325
             function () {
344 326
                 // Interval updates
@@ -363,7 +345,7 @@ StatsCollector.prototype.start = function () {
363 345
                             logger.error("Unsupported key:" + e, e);
364 346
                         }
365 347
 
366
-                        self.baselineStatsReport = self.currentStatsReport;
348
+                        self.previousStatsReport = self.currentStatsReport;
367 349
                     },
368 350
                     self.errorCallback
369 351
                 );
@@ -372,8 +354,7 @@ StatsCollector.prototype.start = function () {
372 354
         );
373 355
     }
374 356
 
375
-    if (this.config.logStats
376
-            && browserSupported
357
+    if (browserSupported
377 358
             // logging statistics does not support firefox
378 359
             && this._browserType !== RTCBrowserType.RTC_BROWSER_FIREFOX) {
379 360
         this.gatherStatsIntervalId = setInterval(
@@ -460,6 +441,7 @@ StatsCollector.prototype._defineGetStatValueMethod = function (keys) {
460 441
     switch (this._browserType) {
461 442
     case RTCBrowserType.RTC_BROWSER_CHROME:
462 443
     case RTCBrowserType.RTC_BROWSER_OPERA:
444
+    case RTCBrowserType.RTC_BROWSER_NWJS:
463 445
         // TODO What about other types of browser which are based on Chrome such
464 446
         // as NW.js? Every time we want to support a new type browser we have to
465 447
         // go and add more conditions (here and in multiple other places).
@@ -467,7 +449,7 @@ StatsCollector.prototype._defineGetStatValueMethod = function (keys) {
467 449
         // example, if item has a stat property of type function, then it's very
468 450
         // likely that whoever defined it wanted you to call it in order to
469 451
         // retrieve the value associated with a specific key.
470
-        itemStatByKey = function (item, key) { return item.stat(key) };
452
+        itemStatByKey = function (item, key) { return item.stat(key); };
471 453
         break;
472 454
     case RTCBrowserType.RTC_BROWSER_REACT_NATIVE:
473 455
         // The implementation provided by react-native-webrtc follows the
@@ -487,14 +469,14 @@ StatsCollector.prototype._defineGetStatValueMethod = function (keys) {
487 469
         };
488 470
         break;
489 471
     default:
490
-        itemStatByKey = function (item, key) { return item[key] };
472
+        itemStatByKey = function (item, key) { return item[key]; };
491 473
     }
492 474
 
493 475
     // Compose the 2 functions defined above to get a function which retrieves
494 476
     // the value from a specific report returned by RTCPeerConnection#getStats
495 477
     // associated with a specific LibJitsiMeet browser-agnostic name.
496 478
     return function (item, name) {
497
-        return itemStatByKey(item, keyFromName(name))
479
+        return itemStatByKey(item, keyFromName(name));
498 480
     };
499 481
 };
500 482
 
@@ -502,11 +484,24 @@ StatsCollector.prototype._defineGetStatValueMethod = function (keys) {
502 484
  * Stats processing logic.
503 485
  */
504 486
 StatsCollector.prototype.processStatsReport = function () {
505
-    if (!this.baselineStatsReport) {
487
+    if (!this.previousStatsReport) {
506 488
         return;
507 489
     }
508 490
 
509 491
     var getStatValue = this._getStatValue;
492
+    function getNonNegativeStat(report, name) {
493
+        var value = getStatValue(report, name);
494
+        if (typeof value !== 'number') {
495
+            value = Number(value);
496
+        }
497
+
498
+        if (isNaN(value)) {
499
+            return 0;
500
+        }
501
+
502
+        return Math.max(0, value);
503
+    }
504
+    var byteSentStats = {};
510 505
 
511 506
     for (var idx in this.currentStatsReport) {
512 507
         var now = this.currentStatsReport[idx];
@@ -538,7 +533,7 @@ StatsCollector.prototype.processStatsReport = function () {
538 533
             var conferenceStatsTransport = this.conferenceStats.transport;
539 534
             if(!conferenceStatsTransport.some(function (t) { return (
540 535
                         t.ip == ip && t.type == type && t.localip == localip
541
-                    )})) {
536
+                    );})) {
542 537
                 conferenceStatsTransport.push(
543 538
                     {ip: ip, type: type, localip: localip});
544 539
             }
@@ -563,18 +558,14 @@ StatsCollector.prototype.processStatsReport = function () {
563 558
             continue;
564 559
         }
565 560
 
566
-        var before = this.baselineStatsReport[idx];
561
+        var before = this.previousStatsReport[idx];
567 562
         var ssrc = getStatValue(now, 'ssrc');
568
-        if (!before) {
569
-            logger.warn(ssrc + ' not enough data');
563
+        if (!before || !ssrc) {
570 564
             continue;
571 565
         }
572 566
 
573
-        if(!ssrc)
574
-            continue;
575
-
576 567
         var ssrcStats
577
-          = this.ssrc2stats[ssrc] || (this.ssrc2stats[ssrc] = new PeerStats());
568
+          = this.ssrc2stats[ssrc] || (this.ssrc2stats[ssrc] = new SsrcStats());
578 569
 
579 570
         var isDownloadStream = true;
580 571
         var key = 'packetsReceived';
@@ -592,61 +583,51 @@ StatsCollector.prototype.processStatsReport = function () {
592 583
         if (!packetsNow || packetsNow < 0)
593 584
             packetsNow = 0;
594 585
 
595
-        var packetsBefore = getStatValue(before, key);
596
-        if (!packetsBefore || packetsBefore < 0)
597
-            packetsBefore = 0;
598
-        var packetRate = packetsNow - packetsBefore;
599
-        if (!packetRate || packetRate < 0)
600
-            packetRate = 0;
601
-        var currentLoss = getStatValue(now, 'packetsLost');
602
-        if (!currentLoss || currentLoss < 0)
603
-            currentLoss = 0;
604
-        var previousLoss = getStatValue(before, 'packetsLost');
605
-        if (!previousLoss || previousLoss < 0)
606
-            previousLoss = 0;
607
-        var lossRate = currentLoss - previousLoss;
608
-        if (!lossRate || lossRate < 0)
609
-            lossRate = 0;
610
-        var packetsTotal = (packetRate + lossRate);
611
-
612
-        ssrcStats.setSsrcLoss({
613
-            packetsTotal: packetsTotal,
614
-            packetsLost: lossRate,
615
-            isDownloadStream: isDownloadStream
616
-        });
586
+        var packetsBefore = getNonNegativeStat(before, key);
587
+        var packetsDiff = Math.max(0, packetsNow - packetsBefore);
617 588
 
618
-        var bytesReceived = 0, bytesSent = 0;
619
-        var nowBytesTransmitted = getStatValue(now, "bytesReceived");
620
-        if(nowBytesTransmitted) {
621
-            bytesReceived
622
-                = nowBytesTransmitted - getStatValue(before, "bytesReceived");
623
-        }
624
-        nowBytesTransmitted = getStatValue(now, "bytesSent");
625
-        if (nowBytesTransmitted) {
626
-            bytesSent = nowBytesTransmitted - getStatValue(before, "bytesSent");
627
-        }
589
+        var packetsLostNow = getNonNegativeStat(now, 'packetsLost');
590
+        var packetsLostBefore = getNonNegativeStat(before, 'packetsLost');
591
+        var packetsLostDiff = Math.max(0, packetsLostNow - packetsLostBefore);
628 592
 
629
-        var time = Math.round((now.timestamp - before.timestamp) / 1000);
630
-        if (bytesReceived <= 0 || time <= 0) {
631
-            bytesReceived = 0;
632
-        } else {
633
-            bytesReceived = Math.round(((bytesReceived * 8) / time) / 1000);
634
-        }
593
+        ssrcStats.setLoss({
594
+            packetsTotal: packetsDiff + packetsLostDiff,
595
+            packetsLost: packetsLostDiff,
596
+            isDownloadStream: isDownloadStream
597
+        });
635 598
 
636
-        if (bytesSent <= 0 || time <= 0) {
637
-            bytesSent = 0;
638
-        } else {
639
-            bytesSent = Math.round(((bytesSent * 8) / time) / 1000);
599
+        var bytesReceivedNow = getNonNegativeStat(now, 'bytesReceived');
600
+        var bytesReceivedBefore = getNonNegativeStat(before, 'bytesReceived');
601
+        var bytesReceived = Math.max(0, bytesReceivedNow - bytesReceivedBefore);
602
+
603
+        var bytesSent = 0;
604
+
605
+        // TODO: clean this mess up!
606
+        var nowBytesTransmitted = getStatValue(now, "bytesSent");
607
+        if(typeof(nowBytesTransmitted) === "number" ||
608
+            typeof(nowBytesTransmitted) === "string") {
609
+            nowBytesTransmitted = Number(nowBytesTransmitted);
610
+            if(!isNaN(nowBytesTransmitted)){
611
+                byteSentStats[ssrc] = nowBytesTransmitted;
612
+                if (nowBytesTransmitted > 0) {
613
+                    bytesSent = nowBytesTransmitted -
614
+                        getStatValue(before, "bytesSent");
615
+                }
616
+            }
640 617
         }
641
-
642
-        //detect audio issues (receiving data but audioLevel == 0)
643
-        if(bytesReceived > 10 && ssrcStats.ssrc2AudioLevel === 0) {
644
-            this.eventEmitter.emit(StatisticsEvents.AUDIO_NOT_WORKING, ssrc);
618
+        bytesSent = Math.max(0, bytesSent);
619
+
620
+        var timeMs = now.timestamp - before.timestamp;
621
+        var bitrateReceivedKbps = 0, bitrateSentKbps = 0;
622
+        if (timeMs > 0) {
623
+            // TODO is there any reason to round here?
624
+            bitrateReceivedKbps = Math.round((bytesReceived * 8) / timeMs);
625
+            bitrateSentKbps = Math.round((bytesSent * 8) / timeMs);
645 626
         }
646 627
 
647
-        ssrcStats.setSsrcBitrate({
648
-            "download": bytesReceived,
649
-            "upload": bytesSent
628
+        ssrcStats.addBitrate({
629
+            "download": bitrateReceivedKbps,
630
+            "upload": bitrateSentKbps
650 631
         });
651 632
 
652 633
         var resolution = {height: null, width: null};
@@ -666,9 +647,9 @@ StatsCollector.prototype.processStatsReport = function () {
666 647
         catch(e){/*not supported*/}
667 648
 
668 649
         if (resolution.height && resolution.width) {
669
-            ssrcStats.setSsrcResolution(resolution);
650
+            ssrcStats.setResolution(resolution);
670 651
         } else {
671
-            ssrcStats.setSsrcResolution(null);
652
+            ssrcStats.setResolution(null);
672 653
         }
673 654
     }
674 655
 
@@ -687,26 +668,26 @@ StatsCollector.prototype.processStatsReport = function () {
687 668
     Object.keys(this.ssrc2stats).forEach(
688 669
         function (ssrc) {
689 670
             var ssrcStats = this.ssrc2stats[ssrc];
690
-
691
-            // process package loss stats
692
-            var ssrc2Loss = ssrcStats.ssrc2Loss;
693
-            var type = ssrc2Loss.isDownloadStream ? "download" : "upload";
694
-            totalPackets[type] += ssrc2Loss.packetsTotal;
695
-            lostPackets[type] += ssrc2Loss.packetsLost;
671
+            // process packet loss stats
672
+            var loss = ssrcStats.loss;
673
+            var type = loss.isDownloadStream ? "download" : "upload";
674
+            totalPackets[type] += loss.packetsTotal;
675
+            lostPackets[type] += loss.packetsLost;
696 676
 
697 677
             // process bitrate stats
698
-            var ssrc2bitrate = ssrcStats.ssrc2bitrate;
699
-            bitrateDownload += ssrc2bitrate.download;
700
-            bitrateUpload += ssrc2bitrate.upload;
678
+            bitrateDownload += ssrcStats.bitrate.download;
679
+            bitrateUpload += ssrcStats.bitrate.upload;
701 680
 
702
-            ssrcStats.resetSsrcBitrate();
681
+            ssrcStats.resetBitrate();
703 682
 
704 683
             // collect resolutions
705
-            resolutions[ssrc] = ssrcStats.ssrc2resolution;
684
+            resolutions[ssrc] = ssrcStats.resolution;
706 685
         },
707 686
         this
708 687
     );
709 688
 
689
+    this.eventEmitter.emit(StatisticsEvents.BYTE_SENT_STATS, byteSentStats);
690
+
710 691
     this.conferenceStats.bitrate
711 692
       = {"upload": bitrateUpload, "download": bitrateDownload};
712 693
 
@@ -720,9 +701,9 @@ StatsCollector.prototype.processStatsReport = function () {
720 701
             calculatePacketLoss(lostPackets.upload, totalPackets.upload)
721 702
     };
722 703
     this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS, {
704
+            "bandwidth": this.conferenceStats.bandwidth,
723 705
             "bitrate": this.conferenceStats.bitrate,
724 706
             "packetLoss": this.conferenceStats.packetLoss,
725
-            "bandwidth": this.conferenceStats.bandwidth,
726 707
             "resolution": resolutions,
727 708
             "transport": this.conferenceStats.transport
728 709
         });
@@ -742,10 +723,8 @@ StatsCollector.prototype.processAudioLevelReport = function () {
742 723
     for (var idx in this.currentAudioLevelsReport) {
743 724
         var now = this.currentAudioLevelsReport[idx];
744 725
 
745
-        //if we don't have "packetsReceived" this is local stream
746
-        if (now.type != 'ssrc' || !getStatValue(now, 'packetsReceived')) {
726
+        if (now.type != 'ssrc')
747 727
             continue;
748
-        }
749 728
 
750 729
         var before = this.baselineAudioLevelsReport[idx];
751 730
         var ssrc = getStatValue(now, 'ssrc');
@@ -760,10 +739,6 @@ StatsCollector.prototype.processAudioLevelReport = function () {
760 739
             continue;
761 740
         }
762 741
 
763
-        var ssrcStats
764
-            = this.ssrc2stats[ssrc]
765
-                || (this.ssrc2stats[ssrc] = new PeerStats());
766
-
767 742
         // Audio level
768 743
         try {
769 744
             var audioLevel
@@ -777,12 +752,13 @@ StatsCollector.prototype.processAudioLevelReport = function () {
777 752
         }
778 753
 
779 754
         if (audioLevel) {
780
-            // TODO: can't find specs about what this value really is,
781
-            // but it seems to vary between 0 and around 32k.
755
+            const isLocal = !getStatValue(now, 'packetsReceived');
756
+
757
+            // TODO: Can't find specs about what this value really is, but it
758
+            // seems to vary between 0 and around 32k.
782 759
             audioLevel = audioLevel / 32767;
783
-            ssrcStats.setSsrcAudioLevel(audioLevel);
784 760
             this.eventEmitter.emit(
785
-                StatisticsEvents.AUDIO_LEVEL, ssrc, audioLevel);
761
+                StatisticsEvents.AUDIO_LEVEL, ssrc, audioLevel, isLocal);
786 762
         }
787 763
     }
788 764
 };

+ 117
- 50
modules/statistics/statistics.js View File

@@ -1,12 +1,18 @@
1 1
 /* global require */
2
-var LocalStats = require("./LocalStatsCollector.js");
2
+var AnalyticsAdapter = require("./AnalyticsAdapter");
3
+var CallStats = require("./CallStats");
4
+var EventEmitter = require("events");
5
+import JitsiTrackError from "../../JitsiTrackError";
3 6
 var logger = require("jitsi-meet-logger").getLogger(__filename);
7
+var LocalStats = require("./LocalStatsCollector.js");
4 8
 var RTPStats = require("./RTPStatsCollector.js");
5
-var EventEmitter = require("events");
6
-var StatisticsEvents = require("../../service/statistics/Events");
7
-var CallStats = require("./CallStats");
8 9
 var ScriptUtil = require('../util/ScriptUtil');
9
-var JitsiTrackError = require("../../JitsiTrackError");
10
+import * as StatisticsEvents from "../../service/statistics/Events";
11
+
12
+/**
13
+ * True if callstats API is loaded
14
+ */
15
+ var isCallstatsLoaded = false;
10 16
 
11 17
 // Since callstats.io is a third party, we cannot guarantee the quality of their
12 18
 // service. More specifically, their server may take noticeably long time to
@@ -15,15 +21,43 @@ var JitsiTrackError = require("../../JitsiTrackError");
15 21
 // allow it to prevent people from joining a conference) to (1) start
16 22
 // downloading their API as soon as possible and (2) do the downloading
17 23
 // asynchronously.
18
-function loadCallStatsAPI() {
19
-    ScriptUtil.loadScript(
20
-            'https://api.callstats.io/static/callstats.min.js',
21
-            /* async */ true,
22
-            /* prepend */ true);
24
+function loadCallStatsAPI(customScriptUrl) {
25
+    if(!isCallstatsLoaded) {
26
+        ScriptUtil.loadScript(
27
+                customScriptUrl ? customScriptUrl :
28
+                    'https://api.callstats.io/static/callstats-ws.min.js',
29
+                /* async */ true,
30
+                /* prepend */ true);
31
+        isCallstatsLoaded = true;
32
+    }
23 33
     // FIXME At the time of this writing, we hope that the callstats.io API will
24 34
     // have loaded by the time we needed it (i.e. CallStats.init is invoked).
25 35
 }
26 36
 
37
+// Load the integration of a third-party analytics API such as Google Analytics.
38
+// Since we cannot guarantee the quality of the third-party service (e.g. their
39
+// server may take noticeably long time to respond), it is in our best interest
40
+// (in the sense that the intergration of the analytics API is important to us
41
+// but not enough to allow it to prevent people from joining a conference) to
42
+// download the API asynchronously. Additionally, Google Analytics will download
43
+// its implementation asynchronously anyway so it makes sense to append the
44
+// loading on our side rather than prepend it.
45
+function loadAnalytics(customScriptUrl) {
46
+    // if we have a custom script url passed as parameter we don't want to
47
+    // search it relatively near the library
48
+    ScriptUtil.loadScript(
49
+        customScriptUrl ? customScriptUrl : 'analytics.js',
50
+        /* async */ true,
51
+        /* prepend */ false,
52
+        /* relativeURL */ customScriptUrl ? false : true,
53
+        /* loadCallback */ function () {
54
+            Statistics.analytics.loaded();
55
+        },
56
+        /* errorCallback */ function () {
57
+            Statistics.analytics.dispose();
58
+        });
59
+}
60
+
27 61
 /**
28 62
  * Log stats via the focus once every this many milliseconds.
29 63
  */
@@ -58,6 +92,25 @@ function formatJitsiTrackErrorForCallStats(error) {
58 92
     return err;
59 93
 }
60 94
 
95
+/**
96
+ * Init statistic options
97
+ * @param options
98
+ */
99
+Statistics.init = function (options) {
100
+    Statistics.audioLevelsEnabled = !options.disableAudioLevels;
101
+
102
+    if(typeof options.audioLevelsInterval === 'number') {
103
+        Statistics.audioLevelsInterval = options.audioLevelsInterval;
104
+    }
105
+
106
+    Statistics.disableThirdPartyRequests = options.disableThirdPartyRequests;
107
+
108
+    if (Statistics.disableThirdPartyRequests !== true)
109
+        loadAnalytics(options.analyticsScriptUrl);
110
+    else // if not enable make sure we dispose any event that goes in the queue
111
+        Statistics.analytics.dispose();
112
+};
113
+
61 114
 function Statistics(xmpp, options) {
62 115
     this.rtpStats = null;
63 116
     this.eventEmitter = new EventEmitter();
@@ -68,10 +121,13 @@ function Statistics(xmpp, options) {
68 121
             // Even though AppID and AppSecret may be specified, the integration
69 122
             // of callstats.io may be disabled because of globally-disallowed
70 123
             // requests to any third parties.
71
-            && (this.options.disableThirdPartyRequests !== true);
124
+            && (Statistics.disableThirdPartyRequests !== true);
72 125
     if(this.callStatsIntegrationEnabled)
73
-        loadCallStatsAPI();
126
+        loadCallStatsAPI(this.options.callStatsCustomScriptUrl);
74 127
     this.callStats = null;
128
+    // Flag indicates whether or not the CallStats have been started for this
129
+    // Statistics instance
130
+    this.callStatsStarted = false;
75 131
 
76 132
     /**
77 133
      * Send the stats already saved in rtpStats to be logged via the focus.
@@ -80,6 +136,8 @@ function Statistics(xmpp, options) {
80 136
 }
81 137
 Statistics.audioLevelsEnabled = false;
82 138
 Statistics.audioLevelsInterval = 200;
139
+Statistics.disableThirdPartyRequests = false;
140
+Statistics.analytics = AnalyticsAdapter;
83 141
 
84 142
 /**
85 143
  * Array of callstats instances. Used to call Statistics static methods and
@@ -88,16 +146,13 @@ Statistics.audioLevelsInterval = 200;
88 146
 Statistics.callsStatsInstances = [];
89 147
 
90 148
 Statistics.prototype.startRemoteStats = function (peerconnection) {
91
-    if(!Statistics.audioLevelsEnabled)
92
-        return;
93
-
94 149
     this.stopRemoteStats();
95 150
 
96 151
     try {
97 152
         this.rtpStats
98 153
             = new RTPStats(peerconnection,
99 154
                     Statistics.audioLevelsInterval, 2000, this.eventEmitter);
100
-        this.rtpStats.start();
155
+        this.rtpStats.start(Statistics.audioLevelsEnabled);
101 156
     } catch (e) {
102 157
         this.rtpStats = null;
103 158
         logger.error('Failed to start collecting remote statistics: ' + e);
@@ -139,34 +194,24 @@ Statistics.prototype.addConnectionStatsListener = function (listener) {
139 194
     this.eventEmitter.on(StatisticsEvents.CONNECTION_STATS, listener);
140 195
 };
141 196
 
142
-/**
143
- * Adds listener for detected audio problems.
144
- * @param listener the listener.
145
- */
146
-Statistics.prototype.addAudioProblemListener = function (listener) {
147
-    this.eventEmitter.on(StatisticsEvents.AUDIO_NOT_WORKING, listener);
148
-};
149
-
150 197
 Statistics.prototype.removeConnectionStatsListener = function (listener) {
151 198
     this.eventEmitter.removeListener(StatisticsEvents.CONNECTION_STATS, listener);
152 199
 };
153 200
 
154
-Statistics.prototype.dispose = function () {
155
-    if(Statistics.audioLevelsEnabled) {
156
-        Statistics.stopAllLocalStats();
157
-        this.stopRemoteStats();
158
-        if(this.eventEmitter)
159
-            this.eventEmitter.removeAllListeners();
160
-    }
201
+Statistics.prototype.addByteSentStatsListener = function (listener) {
202
+    this.eventEmitter.on(StatisticsEvents.BYTE_SENT_STATS, listener);
161 203
 };
162 204
 
163
-Statistics.stopAllLocalStats = function () {
164
-    if(!Statistics.audioLevelsEnabled)
165
-        return;
205
+Statistics.prototype.removeByteSentStatsListener = function (listener) {
206
+    this.eventEmitter.removeListener(StatisticsEvents.BYTE_SENT_STATS,
207
+        listener);
208
+};
166 209
 
167
-    for(var i = 0; i < this.localStats.length; i++)
168
-        this.localStats[i].stop();
169
-    this.localStats = [];
210
+Statistics.prototype.dispose = function () {
211
+    this.stopCallStats();
212
+    this.stopRemoteStats();
213
+    if(this.eventEmitter)
214
+        this.eventEmitter.removeAllListeners();
170 215
 };
171 216
 
172 217
 Statistics.stopLocalStats = function (stream) {
@@ -182,7 +227,7 @@ Statistics.stopLocalStats = function (stream) {
182 227
 };
183 228
 
184 229
 Statistics.prototype.stopRemoteStats = function () {
185
-    if (!Statistics.audioLevelsEnabled || !this.rtpStats) {
230
+    if (!this.rtpStats) {
186 231
         return;
187 232
     }
188 233
 
@@ -204,9 +249,28 @@ Statistics.prototype.stopRemoteStats = function () {
204 249
  * /modules/settings/Settings.js
205 250
  */
206 251
 Statistics.prototype.startCallStats = function (session, settings) {
207
-    if(this.callStatsIntegrationEnabled && !this.callstats) {
252
+    if(this.callStatsIntegrationEnabled && !this.callStatsStarted) {
253
+        // Here we overwrite the previous instance, but it must be bound to
254
+        // the new PeerConnection
208 255
         this.callstats = new CallStats(session, settings, this.options);
209 256
         Statistics.callsStatsInstances.push(this.callstats);
257
+        this.callStatsStarted = true;
258
+    }
259
+};
260
+
261
+/**
262
+ * Removes the callstats.io instances.
263
+ */
264
+Statistics.prototype.stopCallStats = function () {
265
+    if(this.callStatsStarted) {
266
+        var index = Statistics.callsStatsInstances.indexOf(this.callstats);
267
+        if(index > -1)
268
+            Statistics.callsStatsInstances.splice(index, 1);
269
+        // The next line is commented because we need to be able to send
270
+        // feedback even after the conference has been destroyed.
271
+        // this.callstats = null;
272
+        CallStats.dispose();
273
+        this.callStatsStarted = false;
210 274
     }
211 275
 };
212 276
 
@@ -222,12 +286,13 @@ Statistics.prototype.isCallstatsEnabled = function () {
222 286
 };
223 287
 
224 288
 /**
225
- * Notifies CallStats for ice connection failed
289
+ * Notifies CallStats and analytics(if present) for ice connection failed
226 290
  * @param {RTCPeerConnection} pc connection on which failure occured.
227 291
  */
228 292
 Statistics.prototype.sendIceConnectionFailedEvent = function (pc) {
229 293
     if(this.callstats)
230 294
         this.callstats.sendIceConnectionFailedEvent(pc, this.callstats);
295
+    Statistics.analytics.sendEvent('connection.ice_failed');
231 296
 };
232 297
 
233 298
 /**
@@ -372,16 +437,6 @@ Statistics.prototype.sendAddIceCandidateFailed = function (e, pc) {
372 437
         CallStats.sendAddIceCandidateFailed(e, pc, this.callstats);
373 438
 };
374 439
 
375
-/**
376
- * Notifies CallStats that audio problems are detected.
377
- *
378
- * @param {Error} e error to send
379
- */
380
-Statistics.prototype.sendDetectedAudioProblem = function (e) {
381
-    if(this.callstats)
382
-        this.callstats.sendDetectedAudioProblem(e);
383
-};
384
-
385 440
 /**
386 441
  * Adds to CallStats an application log.
387 442
  *
@@ -406,6 +461,8 @@ Statistics.sendLog = function (m) {
406 461
 Statistics.prototype.sendFeedback = function(overall, detailed) {
407 462
     if(this.callstats)
408 463
         this.callstats.sendFeedback(overall, detailed);
464
+    Statistics.analytics.sendEvent("feedback.rating",
465
+        {value: overall, detailed: detailed});
409 466
 };
410 467
 
411 468
 Statistics.LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID;
@@ -423,4 +480,14 @@ Statistics.reportGlobalError = function (error) {
423 480
     }
424 481
 };
425 482
 
483
+/**
484
+ * Sends event to analytics and callstats.
485
+ * @param {string} eventName the event name.
486
+ * @param {Object} data the data to be sent.
487
+ */
488
+Statistics.sendEventToAll = function (eventName, data) {
489
+    this.analytics.sendEvent(eventName, data);
490
+    Statistics.sendLog(JSON.stringify({name: eventName, data}));
491
+};
492
+
426 493
 module.exports = Statistics;

+ 311
- 0
modules/transcription/audioRecorder.js View File

@@ -0,0 +1,311 @@
1
+/* global MediaRecorder, MediaStream, webkitMediaStream */
2
+
3
+var RecordingResult = require("./recordingResult");
4
+
5
+/**
6
+ * Possible audio formats MIME types
7
+ */
8
+var AUDIO_WEBM = "audio/webm";    // Supported in chrome
9
+var AUDIO_OGG  = "audio/ogg";     // Supported in firefox
10
+
11
+/**
12
+ * A TrackRecorder object holds all the information needed for recording a
13
+ * single JitsiTrack (either remote or local)
14
+ * @param track The JitsiTrack the object is going to hold
15
+ */
16
+var TrackRecorder = function(track){
17
+    // The JitsiTrack holding the stream
18
+    this.track = track;
19
+    // The MediaRecorder recording the stream
20
+    this.recorder = null;
21
+    // The array of data chunks recorded from the stream
22
+    // acts as a buffer until the data is stored on disk
23
+    this.data = null;
24
+    //the name of the person of the JitsiTrack. This can be undefined and/or
25
+    //not unique
26
+    this.name = null;
27
+    //the time of the start of the recording
28
+    this.startTime = null;
29
+};
30
+
31
+/**
32
+ * Starts the recording of a JitsiTrack in a TrackRecorder object.
33
+ * This will also define the timestamp and try to update the name
34
+ * @param trackRecorder the TrackRecorder to start
35
+ */
36
+function startRecorder(trackRecorder) {
37
+    if(trackRecorder.recorder === undefined) {
38
+        throw new Error("Passed an object to startRecorder which is not a " +
39
+            "TrackRecorder object");
40
+    }
41
+    trackRecorder.recorder.start();
42
+    trackRecorder.startTime = new Date();
43
+}
44
+
45
+/**
46
+ * Stops the recording of a JitsiTrack in a TrackRecorder object.
47
+ * This will also try to update the name
48
+ * @param trackRecorder the TrackRecorder to stop
49
+ */
50
+function stopRecorder(trackRecorder){
51
+    if(trackRecorder.recorder === undefined) {
52
+        throw new Error("Passed an object to stopRecorder which is not a " +
53
+            "TrackRecorder object");
54
+    }
55
+    trackRecorder.recorder.stop();
56
+}
57
+
58
+/**
59
+ * Creates a TrackRecorder object. Also creates the MediaRecorder and
60
+ * data array for the trackRecorder.
61
+ * @param track the JitsiTrack holding the audio MediaStream(s)
62
+ */
63
+function instantiateTrackRecorder(track) {
64
+    var trackRecorder = new TrackRecorder(track);
65
+    // Create a new stream which only holds the audio track
66
+    var originalStream = trackRecorder.track.getOriginalStream();
67
+    var stream = createEmptyStream();
68
+    originalStream.getAudioTracks().forEach(function(track){
69
+        stream.addTrack(track);
70
+    });
71
+    // Create the MediaRecorder
72
+    trackRecorder.recorder = new MediaRecorder(stream,
73
+        {mimeType: audioRecorder.fileType});
74
+    //array for holding the recorder data. Resets it when
75
+    //audio already has been recorder once
76
+    trackRecorder.data = [];
77
+    // function handling a dataEvent, e.g the stream gets new data
78
+    trackRecorder.recorder.ondataavailable = function (dataEvent) {
79
+        if(dataEvent.data.size > 0) {
80
+            trackRecorder.data.push(dataEvent.data);
81
+        }
82
+    };
83
+
84
+    return trackRecorder;
85
+}
86
+
87
+/**
88
+ * Determines which kind of audio recording the browser supports
89
+ * chrome supports "audio/webm" and firefox supports "audio/ogg"
90
+ */
91
+function determineCorrectFileType() {
92
+    if(MediaRecorder.isTypeSupported(AUDIO_WEBM)) {
93
+        return AUDIO_WEBM;
94
+    }
95
+    else if(MediaRecorder.isTypeSupported(AUDIO_OGG)) {
96
+        return AUDIO_OGG;
97
+    }
98
+    else {
99
+        throw new Error("unable to create a MediaRecorder with the" +
100
+            "right mimetype!");
101
+    }
102
+}
103
+
104
+/**
105
+ * main exported object of the file, holding all
106
+ * relevant functions and variables for the outside world
107
+ * @param jitsiConference the jitsiConference which this object
108
+ * is going to record
109
+ */
110
+var audioRecorder = function(jitsiConference){
111
+    // array of TrackRecorders, where each trackRecorder
112
+    // holds the JitsiTrack, MediaRecorder and recorder data
113
+    this.recorders = [];
114
+
115
+    //get which file type is supported by the current browser
116
+    this.fileType = determineCorrectFileType();
117
+
118
+    //boolean flag for active recording
119
+    this.isRecording = false;
120
+
121
+    //the jitsiconference the object is recording
122
+    this.jitsiConference = jitsiConference;
123
+};
124
+
125
+/**
126
+ * Add the the exported module so that it can be accessed by other files
127
+ */
128
+audioRecorder.determineCorrectFileType = determineCorrectFileType;
129
+
130
+/**
131
+ * Adds a new TrackRecorder object to the array.
132
+ *
133
+ * @param track the track potentially holding an audio stream
134
+ */
135
+audioRecorder.prototype.addTrack = function (track) {
136
+    if(track.isAudioTrack()) {
137
+        //create the track recorder
138
+        var trackRecorder = instantiateTrackRecorder(track);
139
+        //push it to the local array of all recorders
140
+        this.recorders.push(trackRecorder);
141
+        //update the name of the trackRecorders
142
+        this.updateNames();
143
+        //if we're already recording, immediately start recording this new track
144
+        if(this.isRecording){
145
+            startRecorder(trackRecorder);
146
+        }
147
+    }
148
+};
149
+
150
+/**
151
+ * Notifies the module that a specific track has stopped, e.g participant left
152
+ * the conference.
153
+ * if the recording has not started yet, the TrackRecorder will be removed from
154
+ * the array. If the recording has started, the recorder will stop recording
155
+ * but not removed from the array so that the recorded stream can still be
156
+ * accessed
157
+ *
158
+ * @param {JitsiTrack} track the JitsiTrack to remove from the recording session
159
+ */
160
+audioRecorder.prototype.removeTrack = function(track){
161
+    if(track.isVideoTrack()){
162
+        return;
163
+    }
164
+    
165
+    var array = this.recorders;
166
+    var i;
167
+    for(i = 0; i < array.length; i++) {
168
+        if(array[i].track.getParticipantId() === track.getParticipantId()){
169
+            var recorderToRemove = array[i];
170
+            if(this.isRecording){
171
+                stopRecorder(recorderToRemove);
172
+            }
173
+            else {
174
+                //remove the TrackRecorder from the array
175
+                array.splice(i, 1);
176
+            }
177
+        }
178
+    }
179
+
180
+    //make sure the names are up to date
181
+    this.updateNames();
182
+};
183
+
184
+/**
185
+ * Tries to update the name value of all TrackRecorder in the array.
186
+ * If it hasn't changed,it will keep the exiting name. If it changes to a
187
+ * undefined value, the old value will also be kept.
188
+ */
189
+audioRecorder.prototype.updateNames = function(){
190
+    var conference = this.jitsiConference;
191
+    this.recorders.forEach(function(trackRecorder){
192
+        if(trackRecorder.track.isLocal()){
193
+            trackRecorder.name = "the transcriber";
194
+        }
195
+        else {
196
+            var id = trackRecorder.track.getParticipantId();
197
+            var participant = conference.getParticipantById(id);
198
+            var newName = participant.getDisplayName();
199
+            if(newName !== 'undefined') {
200
+                trackRecorder.name = newName;
201
+            }
202
+        }
203
+    });
204
+};
205
+
206
+/**
207
+ * Starts the audio recording of every local and remote track
208
+ */
209
+audioRecorder.prototype.start = function () {
210
+    if(this.isRecording) {
211
+        throw new Error("audiorecorder is already recording");
212
+    }
213
+    // set boolean isRecording flag to true so if new participants join the
214
+    // conference, that track can instantly start recording as well
215
+    this.isRecording = true;
216
+    //start all the mediaRecorders
217
+    this.recorders.forEach(function(trackRecorder){
218
+        startRecorder(trackRecorder);
219
+    });
220
+    //log that recording has started
221
+    console.log("Started the recording of the audio. There are currently " +
222
+        this.recorders.length + " recorders active.");
223
+};
224
+
225
+/**
226
+ * Stops the audio recording of every local and remote track
227
+ */
228
+audioRecorder.prototype.stop = function() {
229
+    //set the boolean flag to false
230
+    this.isRecording = false;
231
+    //stop all recorders
232
+    this.recorders.forEach(function(trackRecorder){
233
+       stopRecorder(trackRecorder);
234
+    });
235
+    console.log("stopped recording");
236
+};
237
+
238
+/**
239
+ * link hacking to download all recorded audio streams
240
+ */
241
+audioRecorder.prototype.download = function () {
242
+    var t = this;
243
+    this.recorders.forEach(function (trackRecorder) {
244
+        var blob = new Blob(trackRecorder.data, {type: t.fileType});
245
+        var url = URL.createObjectURL(blob);
246
+        var a = document.createElement('a');
247
+        document.body.appendChild(a);
248
+        a.style = "display: none";
249
+        a.href = url;
250
+        a.download = 'test.' + t.fileType.split("/")[1];
251
+        a.click();
252
+        window.URL.revokeObjectURL(url);
253
+    });
254
+};
255
+
256
+/**
257
+ * returns the audio files of all recorders as an array of objects,
258
+ * which include the name of the owner of the track and the starting time stamp
259
+ * @returns {Array} an array of RecordingResult objects
260
+ */
261
+audioRecorder.prototype.getRecordingResults = function () {
262
+    if(this.isRecording) {
263
+        throw new Error("cannot get blobs because the AudioRecorder is still" +
264
+            "recording!");
265
+    }
266
+    //make sure the names are up to date before sending them off
267
+    this.updateNames();
268
+
269
+    var array = [];
270
+    var t = this;
271
+    this.recorders.forEach(function (recorder) {
272
+        array.push(
273
+            new RecordingResult(
274
+            new Blob(recorder.data, {type: t.fileType}),
275
+            recorder.name,
276
+            recorder.startTime)
277
+        );
278
+    });
279
+    return array;
280
+};
281
+
282
+/**
283
+ * Gets the mime type of the recorder audio
284
+ * @returns {String} the mime type of the recorder audio
285
+ */
286
+audioRecorder.prototype.getFileType = function () {
287
+    return this.fileType;
288
+};
289
+
290
+/**
291
+ * Creates a empty MediaStream object which can be used
292
+ * to add MediaStreamTracks to
293
+ * @returns MediaStream
294
+ */
295
+function createEmptyStream() {
296
+    // Firefox supports the MediaStream object, Chrome webkitMediaStream
297
+    if(typeof(MediaStream) !== 'undefined') {
298
+        return new MediaStream();
299
+    }
300
+    else if(typeof(webkitMediaStream) !== 'undefined') {
301
+        return new webkitMediaStream(); // eslint-disable-line new-cap
302
+    }
303
+    else {
304
+        throw new Error("cannot create a clean mediaStream");
305
+    }
306
+}
307
+
308
+/**
309
+ * export the main object audioRecorder
310
+ */
311
+module.exports = audioRecorder;

+ 18
- 0
modules/transcription/recordingResult.js View File

@@ -0,0 +1,18 @@
1
+/**
2
+ * This object stores variables needed around the recording of an audio stream
3
+ * and passing this recording along with additional information along to
4
+ * different processes
5
+ * @param blob the recording audio stream as a single blob
6
+ * @param name the name of the person of the audio stream
7
+ * @param startTime the time in UTC when recording of the audiostream started
8
+ * @param wordArray the recorder audio stream transcribed as an array of Word
9
+ *                  objects
10
+ */
11
+var RecordingResult = function(blob, name, startTime, wordArray){
12
+    this.blob = blob;
13
+    this.name = name;
14
+    this.startTime = startTime;
15
+    this.wordArray = wordArray;
16
+};
17
+
18
+module.exports = RecordingResult;

+ 328
- 0
modules/transcription/transcriber.js View File

@@ -0,0 +1,328 @@
1
+var AudioRecorder = require( './audioRecorder');
2
+var SphinxService = require(
3
+    './transcriptionServices/SphinxTranscriptionService');
4
+
5
+var BEFORE_STATE = "before";
6
+var RECORDING_STATE = "recording";
7
+var TRANSCRIBING_STATE = "transcribing";
8
+var FINISHED_STATE = "finished";
9
+
10
+//the amount of characters each line in the transcription will have
11
+var MAXIMUM_SENTENCE_LENGTH = 80;
12
+
13
+/**
14
+ * This is the main object for handing the Transcription. It interacts with
15
+ * the audioRecorder to record every person in a conference and sends the
16
+ * recorder audio to a transcriptionService. The returned speech-to-text result
17
+ * will be merged to create a transcript
18
+ * @param {AudioRecorder} audioRecorder An audioRecorder recording a conference
19
+ */
20
+var transcriber = function() {
21
+    //the object which can record all audio in the conference
22
+    this.audioRecorder = new AudioRecorder();
23
+    //this object can send the recorder audio to a speech-to-text service
24
+    this.transcriptionService =  new SphinxService();
25
+    //holds a counter to keep track if merging can start
26
+    this.counter = null;
27
+    //holds the date when transcription started which makes it possible
28
+    //to calculate the offset between recordings
29
+    this.startTime = null;
30
+    //will hold the transcription once it is completed
31
+    this.transcription = null;
32
+    //this will be a method which will be called once the transcription is done
33
+    //with the transcription as parameter
34
+    this.callback =  null;
35
+    //stores all the retrieved speech-to-text results to merge together
36
+    //this value will store an Array<Word> object
37
+    this.results = [];
38
+    // Stores the current state of the transcription process
39
+    this.state = BEFORE_STATE;
40
+    //Used in the updateTranscription method to add a new line when the
41
+    //sentence becomes to long
42
+    this.lineLength = 0;
43
+};
44
+
45
+/**
46
+ * Method to start the transcription process. It will tell the audioRecorder
47
+ * to start storing all audio streams and record the start time for merging
48
+ * purposes
49
+ */
50
+transcriber.prototype.start = function start() {
51
+    if(this.state !== BEFORE_STATE){
52
+        throw new Error("The transcription can only start when it's in the" +
53
+            "\"" + BEFORE_STATE + "\" state. It's currently in the " +
54
+            "\"" + this.state + "\" state");
55
+    }
56
+    this.state = RECORDING_STATE;
57
+    this.audioRecorder.start();
58
+    this.startTime = new Date();
59
+};
60
+
61
+/**
62
+ * Method to stop the transcription process. It will tell the audioRecorder to
63
+ * stop, and get all the recorded audio to send it to the transcription service
64
+
65
+ * @param callback a callback which will receive the transcription
66
+ */
67
+transcriber.prototype.stop = function stop(callback) {
68
+    if(this.state !== RECORDING_STATE){
69
+        throw new Error("The transcription can only stop when it's in the" +
70
+            "\"" + RECORDING_STATE + "\" state. It's currently in the " +
71
+            "\"" + this.state + "\" state");
72
+    }
73
+    //stop the recording
74
+    console.log("stopping recording and sending audio files");
75
+    this.audioRecorder.stop();
76
+    //and send all recorded audio the the transcription service
77
+    var t = this;
78
+
79
+    var callBack = blobCallBack.bind(this);
80
+    this.audioRecorder.getRecordingResults().forEach(function(recordingResult){
81
+        t.transcriptionService.send(recordingResult, callBack);
82
+        t.counter++;
83
+    });
84
+    //set the state to "transcribing" so that maybeMerge() functions correctly
85
+    this.state = TRANSCRIBING_STATE;
86
+    //and store the callback for later
87
+    this.callback = callback;
88
+};
89
+
90
+/**
91
+ * This method gets the answer from the transcription service, calculates the
92
+ * offset and adds is to every Word object. It will also start the merging
93
+ * when every send request has been received
94
+ *
95
+ * note: Make sure to bind this as a Transcription object
96
+ *
97
+ * @param {RecordingResult} answer a RecordingResult object with a defined
98
+ * WordArray
99
+ */
100
+var blobCallBack = function(answer){
101
+    console.log("retrieved an answer from the transcription service. The" +
102
+        " answer has an array of length: " + answer.wordArray.length);
103
+    //first add the offset between the start of the transcription and
104
+    //the start of the recording to all start and end times
105
+    if(answer.wordArray.length > 0) {
106
+        var offset = answer.startTime.getUTCMilliseconds() -
107
+            this.startTime.getUTCMilliseconds();
108
+        //transcriber time will always be earlier
109
+        if (offset < 0) {
110
+            offset = 0; //presume 0 if it somehow not earlier
111
+        }
112
+
113
+        var array = "[";
114
+        answer.wordArray.forEach(function(wordObject) {
115
+            wordObject.begin += offset;
116
+            wordObject.end += offset;
117
+            array += wordObject.word+",";
118
+        });
119
+        array += "]";
120
+        console.log(array);
121
+        //give a name value to the Array object so that the merging can access
122
+        //the name value without having to use the whole recordingResult object
123
+        //in the algorithm
124
+        answer.wordArray.name = answer.name;
125
+    }
126
+    //then store the array and decrease the counter
127
+    this.results.push(answer.wordArray);
128
+    this.counter--;
129
+    console.log("current counter: " + this.counter);
130
+    //and check if all results have been received.
131
+    this.maybeMerge();
132
+};
133
+
134
+/**
135
+ * this method will check if the counter is zero. If it is, it will call
136
+ * the merging method
137
+ */
138
+transcriber.prototype.maybeMerge = function(){
139
+    if(this.state === TRANSCRIBING_STATE && this.counter === 0){
140
+        //make sure to include the events in the result arrays before
141
+        //merging starts
142
+        this.merge();
143
+    }
144
+};
145
+
146
+/**
147
+ * This method will merge all speech-to-text arrays together in one
148
+ * readable transcription string
149
+ */
150
+transcriber.prototype.merge = function() {
151
+    console.log("starting merge process!\n The length of the array: " +
152
+        this.results.length);
153
+    this.transcription = "";
154
+    //the merging algorithm will look over all Word objects who are at pos 0 in
155
+    //every array. It will then select the one closest in time to the
156
+    //previously placed word, while removing the selected word from its array
157
+    //note: words can be skipped the skipped word's begin and end time somehow
158
+    //end up between the closest word start and end time
159
+    var arrays = this.results;
160
+    //arrays of Word objects
161
+    var potentialWords = []; //array of the first Word objects
162
+    //check if any arrays are already empty and remove them
163
+    hasPopulatedArrays(arrays);
164
+
165
+    //populate all the potential Words for a first time
166
+    arrays.forEach(function (array){
167
+        pushWordToSortedArray(potentialWords, array);
168
+    });
169
+
170
+    //keep adding words to transcription until all arrays are exhausted
171
+    var lowestWordArray;
172
+    var wordToAdd;
173
+    var foundSmaller;
174
+    while(hasPopulatedArrays(arrays)){
175
+        //first select the lowest array;
176
+        lowestWordArray = arrays[0];
177
+        arrays.forEach(function(wordArray){
178
+           if(wordArray[0].begin < lowestWordArray[0].begin){
179
+               lowestWordArray = wordArray;
180
+           }
181
+        });
182
+        //put the word in the transcription
183
+        wordToAdd = lowestWordArray.shift();
184
+        this.updateTranscription(wordToAdd,lowestWordArray.name);
185
+
186
+        //keep going until a word in another array has a smaller time
187
+        //or the array is empty
188
+        while(!foundSmaller && lowestWordArray.length > 0){
189
+            arrays.forEach(function(wordArray){
190
+                if(wordArray[0].begin < lowestWordArray[0].begin){
191
+                    foundSmaller = true;
192
+                }
193
+            });
194
+            //add next word if no smaller time has been found
195
+            if(!foundSmaller){
196
+                wordToAdd = lowestWordArray.shift();
197
+                this.updateTranscription(wordToAdd, null);
198
+            }
199
+        }
200
+
201
+    }
202
+
203
+    //set the state to finished and do the necessary left-over tasks
204
+    this.state = FINISHED_STATE;
205
+    if(this.callback){
206
+        this.callback(this.transcription);
207
+    }
208
+};
209
+
210
+/**
211
+ * Appends a word object to the transcription. It will make a new line with a
212
+ * name if a name is specified
213
+ * @param {Word} word the Word object holding the word to append
214
+ * @param {String|null} name the name of a new speaker. Null if not applicable
215
+ */
216
+transcriber.prototype.updateTranscription = function(word, name){
217
+    if(name !== undefined && name !== null){
218
+        this.transcription += "\n" + name + ":";
219
+        this.lineLength = name.length + 1; //+1 for the semi-colon
220
+    }
221
+    if(this.lineLength + word.word.length > MAXIMUM_SENTENCE_LENGTH){
222
+        this.transcription += "\n    ";
223
+        this.lineLength = 4; //because of the 4 spaces after the new line
224
+    }
225
+    this.transcription += " " + word.word;
226
+    this.lineLength += word.word.length + 1; //+1 for the space
227
+};
228
+
229
+/**
230
+ * Check if the given 2 dimensional array has any non-zero Word-arrays in them.
231
+ * All zero-element arrays inside will be removed
232
+ * If any non-zero-element arrays are found, the method will return true.
233
+ * otherwise it will return false
234
+ * @param {Array<Array>} twoDimensionalArray the array to check
235
+ * @returns {boolean} true if any non-zero arrays inside, otherwise false
236
+ */
237
+var hasPopulatedArrays = function(twoDimensionalArray){
238
+    var i;
239
+    for(i = 0; i < twoDimensionalArray.length; i++){
240
+        if(twoDimensionalArray[i].length === 0){
241
+            twoDimensionalArray.splice(i, 1);
242
+        }
243
+    }
244
+    return twoDimensionalArray.length > 0;
245
+};
246
+
247
+/**
248
+ * Push a word to the right location in a sorted array. The array is sorted
249
+ * from lowest to highest start time. Every word is stored in an object which
250
+ * includes the name of the person saying the word.
251
+ *
252
+ * @param {Array<Word>} array the sorted array to push to
253
+ * @param {Word} word the word to push into the array
254
+ */
255
+var pushWordToSortedArray = function(array, word){
256
+    if(array.length === 0) {
257
+        array.push(word);
258
+    }
259
+    else{
260
+        if(array[array.length - 1].begin <= word.begin){
261
+            array.push(word);
262
+            return;
263
+        }
264
+        var i;
265
+        for(i = 0; i < array.length; i++){
266
+            if(word.begin < array[i].begin){
267
+                array.splice(i, 0, word);
268
+                return;
269
+            }
270
+        }
271
+        array.push(word); //fail safe
272
+    }
273
+};
274
+
275
+/**
276
+ * Gives the transcriber a JitsiTrack holding an audioStream to transcribe.
277
+ * The JitsiTrack is given to the audioRecorder. If it doesn't hold an
278
+ * audiostream, it will not be added by the audioRecorder
279
+ * @param {JitsiTrack} track the track to give to the audioRecorder
280
+ */
281
+transcriber.prototype.addTrack = function(track){
282
+    this.audioRecorder.addTrack(track);
283
+};
284
+
285
+/**
286
+ * Remove the given track from the auioRecorder
287
+ * @param track
288
+ */
289
+transcriber.prototype.removeTrack = function(track){
290
+    this.audioRecorder.removeTrack(track);
291
+};
292
+
293
+/**
294
+ * Will return the created transcription if it's avialable or throw an error
295
+ * when it's not done yet
296
+ * @returns {String} the transcription as a String
297
+ */
298
+transcriber.prototype.getTranscription = function(){
299
+    if(this.state !== FINISHED_STATE){
300
+        throw new Error("The transcription can only be retrieved when it's in" +
301
+            " the\"" + FINISHED_STATE + "\" state. It's currently in the " +
302
+            "\"" + this.state + "\" state");
303
+    }
304
+    return this.transcription;
305
+};
306
+
307
+/**
308
+ * Returns the current state of the transcription process
309
+ */
310
+transcriber.prototype.getState = function(){
311
+    return this.state;
312
+};
313
+
314
+/**
315
+ * Resets the state to the "before" state, such that it's again possible to
316
+ * call the start method
317
+ */
318
+transcriber.prototype.reset = function() {
319
+    this.state = BEFORE_STATE;
320
+    this.counter = null;
321
+    this.transcription = null;
322
+    this.startTime = null;
323
+    this.callback = null;
324
+    this.results = [];
325
+    this.lineLength = 0;
326
+};
327
+
328
+module.exports = transcriber;

+ 16
- 0
modules/transcription/transcriberHolder.js View File

@@ -0,0 +1,16 @@
1
+/**
2
+ * This objets holds all created transcriberHolders so that they can be
3
+ * accessed through the JitsiMeetJS object.
4
+ *
5
+ * This is probably temporary until there is a better way to expose the
6
+ * Transcriber in a conference
7
+ */
8
+var transcriberHolder = {
9
+    transcribers : [],
10
+
11
+    add : function(transcriber){
12
+        transcriberHolder.transcribers.push(transcriber);
13
+    }
14
+};
15
+
16
+module.exports = transcriberHolder;

+ 80
- 0
modules/transcription/transcriptionServices/AbstractTranscriptionService.js View File

@@ -0,0 +1,80 @@
1
+/**
2
+ * Abstract class representing an interface to implement a speech-to-text
3
+ * service on.
4
+ */
5
+var TranscriptionService = function() {
6
+    throw new Error("TranscriptionService is abstract and cannot be" +
7
+        "created");
8
+};
9
+
10
+/**
11
+ * This method can be used to send the recorder audio stream and
12
+ * retrieve the answer from the transcription service from the callback
13
+ *
14
+ * @param {RecordingResult} recordingResult a recordingResult object which
15
+ * includes the recorded audio stream as a blob
16
+ * @param {Function} callback  which will retrieve the a RecordingResult with
17
+ *        the answer as a WordArray
18
+ */
19
+TranscriptionService.prototype.send = function send(recordingResult, callback){
20
+    var t = this;
21
+    this.sendRequest(recordingResult.blob, function(response){
22
+        if(!t.verify(response)){
23
+               console.log("the retrieved response from the server" +
24
+                   " is not valid!");
25
+            recordingResult.wordArray = [];
26
+            callback(recordingResult);
27
+        }
28
+        else{
29
+            recordingResult.wordArray = t.formatResponse(response);
30
+            callback(recordingResult);
31
+        }
32
+    });
33
+};
34
+
35
+/**
36
+ * Abstract method which will rend the recorder audio stream to the implemented
37
+ * transcription service and will retrieve an answer, which will be
38
+ * called on the given callback method
39
+ *
40
+ * @param {Blob} audioBlob the recorded audio stream as a single Blob
41
+ * @param {function} callback function which will retrieve the answer
42
+ *                            from the service
43
+ */
44
+// eslint-disable-next-line no-unused-vars
45
+TranscriptionService.prototype.sendRequest = function(audioBlob, callback) {
46
+    throw new Error("TranscriptionService.sendRequest is abstract");
47
+};
48
+
49
+/**
50
+ * Abstract method which will parse the output from the implemented
51
+ * transcription service to the expected format
52
+ *
53
+ * The transcriber class expect an array of word objects, where each word
54
+ * object is one transcribed word by the service.
55
+ *
56
+ * The expected output of this method is an array of word objects, in
57
+ * the correct order. That is, the first object in the array is the first word
58
+ * being said, and the last word in the array is the last word being said
59
+ *
60
+ * @param response the answer from the speech-to-text server which needs to be
61
+ *                 formatted
62
+ * @return {Array<Word>} an array of Word objects
63
+ */
64
+// eslint-disable-next-line no-unused-vars
65
+TranscriptionService.prototype.formatResponse = function(response){
66
+    throw new Error("TranscriptionService.format is abstract");
67
+};
68
+
69
+/**
70
+ * Abstract method which will verify that the response from the server is valid
71
+ *
72
+ * @param response the response from the server
73
+ * @return {boolean} true if response is valid, false otherwise
74
+ */
75
+// eslint-disable-next-line no-unused-vars
76
+TranscriptionService.prototype.verify = function(response){
77
+      throw new Error("TranscriptionService.verify is abstract");
78
+};
79
+
80
+module.exports = TranscriptionService;

+ 130
- 0
modules/transcription/transcriptionServices/SphinxTranscriptionService.js View File

@@ -0,0 +1,130 @@
1
+/* global config */
2
+
3
+var TranscriptionService = require("./AbstractTranscriptionService");
4
+var Word = require( "../word");
5
+var audioRecorder = require("./../audioRecorder");
6
+
7
+/**
8
+ * Implements a TranscriptionService for a Sphinx4 http server
9
+ */
10
+var SphinxService = function() {
11
+    //set the correct url
12
+    this.url = getURL();
13
+};
14
+
15
+/**
16
+ * Subclass of AbstractTranscriptionService
17
+ */
18
+SphinxService.prototype = Object.create(TranscriptionService.prototype);
19
+
20
+/**
21
+ * Set the right constructor
22
+ */
23
+SphinxService.constructor = SphinxService;
24
+
25
+/**
26
+ * Overrides the sendRequest method from AbstractTranscriptionService
27
+ * it will send the audio stream the a Sphinx4 server to get the transcription
28
+ *
29
+ * @param audioFileBlob the recorder audio stream an a single Blob
30
+ * @param callback the callback function retrieving the server response
31
+ */
32
+SphinxService.prototype.sendRequest = function(audioFileBlob, callback) {
33
+    console.log("sending an audio file  to " + this.url);
34
+    console.log("the audio file being sent: " + audioFileBlob);
35
+    var request = new XMLHttpRequest();
36
+    request.onreadystatechange = function() {
37
+        if(request.readyState === XMLHttpRequest.DONE && request.status === 200)
38
+        {
39
+            callback(request.responseText);
40
+        }
41
+        else if (request.readyState === XMLHttpRequest.DONE) {
42
+            throw new Error("unable to accept response from sphinx server." +
43
+                "status: " + request.status);
44
+        }
45
+        //if not ready no point to throw an error
46
+    };
47
+    request.open("POST", this.url);
48
+    request.setRequestHeader("Content-Type",
49
+        audioRecorder.determineCorrectFileType());
50
+    request.send(audioFileBlob);
51
+    console.log("send " + audioFileBlob);
52
+};
53
+
54
+/**
55
+ * Overrides the formatResponse method from AbstractTranscriptionService
56
+ * It will parse the answer from the server in the expected format
57
+ *
58
+ * @param response the JSON body retrieved from the Sphinx4 server
59
+ */
60
+SphinxService.prototype.formatResponse = function(response) {
61
+    var result = JSON.parse(response).objects;
62
+    //make sure to delete the session id object, which is always
63
+    //the first value in the JSON array
64
+    result.shift();
65
+    var array = [];
66
+    result.forEach(function(word){
67
+        if(!word.filler) {
68
+            array.push(new Word(word.word, word.start, word.end));
69
+        }
70
+    });
71
+    return array;
72
+};
73
+
74
+/**
75
+ * checks wether the reply is empty, or doesn't contain a correct JSON object
76
+ * @param response the server response
77
+ * @return {boolean} whether the response is valid
78
+ */
79
+SphinxService.prototype.verify = function(response){
80
+    console.log("response from server:" + response.toString());
81
+    //test if server responded with a string object
82
+    if(typeof(response) !== "string"){
83
+        return false;
84
+    }
85
+    //test if the string can be parsed into valid JSON
86
+    var json;
87
+    try{
88
+        json = JSON.parse(response);
89
+    }
90
+    catch (error){
91
+        console.log(error);
92
+        return false;
93
+    }
94
+    //check if the JSON has a "objects" value
95
+    if(json.objects === undefined){
96
+        return false;
97
+    }
98
+    //get the "objects" value and check for a session ID
99
+    var array = json.objects;
100
+    if(!(array[0] && array[0]["session-id"])){
101
+        return false;
102
+    }
103
+    //everything seems to be in order
104
+    return true;
105
+};
106
+
107
+/**
108
+ * Gets the URL to the Sphinx4 server from the config file. If it's not there,
109
+ * it will throw an error
110
+ *
111
+ * @returns {string} the URL to the sphinx4 server
112
+ */
113
+function getURL() {
114
+    var message = "config does not contain an url to a " +
115
+    "Sphinx4 https server";
116
+    if(config.sphinxURL === undefined){
117
+        console.log(message);
118
+    }
119
+    else {
120
+        var toReturn = config.sphinxURL;
121
+        if(toReturn.includes !== undefined && toReturn.includes("https://")){
122
+            return toReturn;
123
+        }
124
+        else{
125
+            console.log(message);
126
+        }
127
+    }
128
+}
129
+
130
+module.exports = SphinxService;

+ 37
- 0
modules/transcription/word.js View File

@@ -0,0 +1,37 @@
1
+/**
2
+ * An object representing a transcribed word, with some additional information
3
+ * @param word the word 
4
+ * @param begin the time the word was started being uttered
5
+ * @param end the time the word stopped being uttered
6
+ */
7
+var Word = function (word, begin, end) {
8
+    this.word = word;
9
+    this.begin = begin;
10
+    this.end = end;
11
+};
12
+
13
+/**
14
+ * Get the string representation of the word
15
+ * @returns {*} the word as a string
16
+ */
17
+Word.prototype.getWord = function() {
18
+    return this.word;  
19
+};
20
+
21
+/**
22
+ * Get the time the word started being uttered
23
+ * @returns {*} the start time as an integer
24
+ */
25
+Word.prototype.getBeginTime = function () {
26
+    return this.begin;
27
+};
28
+
29
+/**
30
+ * Get the time the word stopped being uttered
31
+ * @returns {*} the end time as an integer
32
+ */
33
+Word.prototype.getEndTime = function () {
34
+    return this.end;
35
+};
36
+
37
+module.exports = Word;

+ 35
- 0
modules/util/EventEmitterForwarder.js View File

@@ -0,0 +1,35 @@
1
+/**
2
+ * Implements utility to forward events from one eventEmitter to another.
3
+ * @param src {object} instance of EventEmitter or another class that implements
4
+ * addListener method which will register listener to EventEmitter instance.
5
+ * @param dest {object} instance of EventEmitter or another class that
6
+ * implements emit method which will emit an event.
7
+ */
8
+function EventEmitterForwarder (src, dest) {
9
+    if (!src || !dest || typeof(src.addListener) !== "function" ||
10
+        typeof(dest.emit) !== "function")
11
+        throw new Error("Invalid arguments passed to EventEmitterForwarder");
12
+    this.src = src;
13
+    this.dest = dest;
14
+}
15
+
16
+/**
17
+ * Adds event to be forwarded from src to dest.
18
+ * @param srcEvent {string} the event that EventEmitterForwarder is listening
19
+ * for.
20
+ * @param dstEvent {string} the event that will be fired from dest.
21
+ * @param arguments all other passed arguments are going to be fired with
22
+ * dstEvent.
23
+ */
24
+EventEmitterForwarder.prototype.forward = function () {
25
+    // This line is only for fixing jshint errors.
26
+    var args = arguments;
27
+    var srcEvent = args[0];
28
+    //This will be the "this" value for emit function.
29
+    args[0] = this.dest;
30
+    //Using bind.apply to pass the arguments as Array-like object ("arguments")
31
+    this.src.addListener(srcEvent,
32
+        Function.prototype.bind.apply(this.dest.emit, args));
33
+};
34
+
35
+module.exports = EventEmitterForwarder;

+ 30
- 2
modules/util/ScriptUtil.js View File

@@ -1,3 +1,6 @@
1
+var currentExecutingScript = require("current-executing-script");
2
+
3
+
1 4
 /**
2 5
  * Implements utility functions which facilitate the dealing with scripts such
3 6
  * as the download and execution of a JavaScript file.
@@ -12,21 +15,46 @@ var ScriptUtil = {
12 15
      * @param prepend true to schedule the loading of the script as soon as
13 16
      * possible or false to schedule the loading of the script at the end of the
14 17
      * scripts known at the time
18
+     * @param relativeURL whether we need load the library from url relative
19
+     * to the url that lib-jitsi-meet was loaded. Useful when sourcing the
20
+     * library from different location than the app that is using it
21
+     * @param loadCallback on load callback function
22
+     * @param errorCallback callback to be called on error loading the script
15 23
      */
16
-    loadScript: function (src, async, prepend) {
24
+    loadScript: function (src, async, prepend, relativeURL,
25
+                          loadCallback, errorCallback) {
17 26
         var d = document;
18 27
         var tagName = 'script';
19 28
         var script = d.createElement(tagName);
20 29
         var referenceNode = d.getElementsByTagName(tagName)[0];
21 30
 
22 31
         script.async = async;
32
+
33
+        if (relativeURL) {
34
+            // finds the src url of the current loaded script
35
+            // and use it as base of the src supplied argument
36
+            var scriptEl = currentExecutingScript();
37
+            if(scriptEl) {
38
+                var scriptSrc = scriptEl.src;
39
+                var baseScriptSrc
40
+                    = scriptSrc.substring(0, scriptSrc.lastIndexOf('/') + 1);
41
+                if (scriptSrc && baseScriptSrc)
42
+                    src = baseScriptSrc + src;
43
+            }
44
+        }
45
+
46
+        if (loadCallback)
47
+            script.onload = loadCallback;
48
+        if (errorCallback)
49
+            script.onerror = errorCallback;
50
+
23 51
         script.src = src;
24 52
         if (prepend) {
25 53
             referenceNode.parentNode.insertBefore(script, referenceNode);
26 54
         } else {
27 55
             referenceNode.parentNode.appendChild(script);
28 56
         }
29
-    },
57
+    }
30 58
 };
31 59
 
32 60
 module.exports = ScriptUtil;

+ 16
- 14
modules/version/ComponentsVersions.js View File

@@ -12,42 +12,43 @@ ComponentsVersions.FOCUS_COMPONENT = "focus";
12 12
  */
13 13
 ComponentsVersions.VIDEOBRIDGE_COMPONENT = "videobridge";
14 14
 /**
15
- * The contant for the name of the XMPP server component.
15
+ * The constant for the name of the XMPP server component.
16 16
  * @type {string}
17 17
  */
18 18
 ComponentsVersions.XMPP_SERVER_COMPONENT = "xmpp";
19 19
 
20 20
 /**
21 21
  * Creates new instance of <tt>ComponentsVersions</tt> which will be discovering
22
- * the versions of conferencing system components in given <tt>ChatRoom</tt>.
23
- * @param chatRoom <tt>ChatRoom</tt> instance which will be used to listen for
24
- *        focus presence updates.
22
+ * the versions of conferencing system components in given
23
+ * <tt>JitsiConference</tt>.
24
+ * @param conference <tt>JitsiConference</tt> instance which will be used to
25
+ *        listen for focus presence updates.
25 26
  * @constructor
26 27
  */
27
-function ComponentsVersions(chatRoom) {
28
+function ComponentsVersions(conference) {
28 29
 
29 30
     this.versions = {};
30 31
 
31
-    this.chatRoom = chatRoom;
32
-    this.chatRoom.addPresenceListener(
32
+    this.conference = conference;
33
+    this.conference.addCommandListener(
33 34
         'versions', this.processPresence.bind(this));
34 35
 }
35 36
 
36 37
 ComponentsVersions.prototype.processPresence =
37
-function(node, mucResource, mucJid) {
38
+    function(node, mucResource, mucJid) {
38 39
 
39 40
     if (node.attributes.xmlns !== 'http://jitsi.org/jitmeet') {
40 41
         logger.warn("Ignored presence versions node - invalid xmlns", node);
41 42
         return;
42 43
     }
43 44
 
44
-    if (!this.chatRoom.isFocus(mucJid)) {
45
+    if (!this.conference._isFocus(mucJid)) {
45 46
         logger.warn(
46 47
             "Received versions not from the focus user: " + node, mucJid);
47 48
         return;
48 49
     }
49 50
 
50
-    var log = "";
51
+    var log = [];
51 52
     node.children.forEach(function(item){
52 53
 
53 54
         var componentName = item.attributes.name;
@@ -65,14 +66,16 @@ function(node, mucResource, mucJid) {
65 66
             this.versions[componentName] = version;
66 67
             logger.info("Got " + componentName + " version: " + version);
67 68
 
68
-            log += (log.length > 0? ", " : "")
69
-                + componentName + ": " + version;
69
+            log.push({
70
+                id: "component_version",
71
+                component: componentName,
72
+                version: version});
70 73
         }
71 74
     }.bind(this));
72 75
 
73 76
     // logs versions to stats
74 77
     if (log.length > 0)
75
-        Statistics.sendLog(log);
78
+        Statistics.sendLog(JSON.stringify(log));
76 79
 };
77 80
 
78 81
 /**
@@ -87,4 +90,3 @@ ComponentsVersions.prototype.getComponentVersion = function(componentName) {
87 90
 };
88 91
 
89 92
 module.exports = ComponentsVersions;
90
-

+ 168
- 67
modules/xmpp/ChatRoom.js View File

@@ -1,6 +1,7 @@
1 1
 /* global Strophe, $, $pres, $iq, $msg */
2
-/* jshint -W101,-W069 */
3
-var logger = require("jitsi-meet-logger").getLogger(__filename);
2
+
3
+import {getLogger} from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
4 5
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
5 6
 var MediaType = require("../../service/RTC/MediaType");
6 7
 var Moderator = require("./moderator");
@@ -8,14 +9,12 @@ var EventEmitter = require("events");
8 9
 var Recorder = require("./recording");
9 10
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
10 11
 
11
-var JIBRI_XMLNS = 'http://jitsi.org/protocol/jibri';
12
-
13 12
 var parser = {
14 13
     packet2JSON: function (packet, nodes) {
15 14
         var self = this;
16
-        $(packet).children().each(function (index) {
15
+        $(packet).children().each(function () {
17 16
             var tagName = $(this).prop("tagName");
18
-            var node = {
17
+            const node = {
19 18
                 tagName: tagName
20 19
             };
21 20
             node.attributes = {};
@@ -32,8 +31,8 @@ var parser = {
32 31
         });
33 32
     },
34 33
     JSON2packet: function (nodes, packet) {
35
-        for(var i = 0; i < nodes.length; i++) {
36
-            var node = nodes[i];
34
+        for(let i = 0; i < nodes.length; i++) {
35
+            const node = nodes[i];
37 36
             if(!node || node === null){
38 37
                 continue;
39 38
             }
@@ -55,7 +54,7 @@ var parser = {
55 54
  */
56 55
 function filterNodeFromPresenceJSON(pres, nodeName){
57 56
     var res = [];
58
-    for(var i = 0; i < pres.length; i++)
57
+    for(let i = 0; i < pres.length; i++)
59 58
         if(pres[i].tagName === nodeName)
60 59
             res.push(pres[i]);
61 60
 
@@ -74,20 +73,21 @@ function ChatRoom(connection, jid, password, XMPP, options, settings) {
74 73
     this.presMap = {};
75 74
     this.presHandlers = {};
76 75
     this.joined = false;
77
-    this.role = 'none';
76
+    this.role = null;
78 77
     this.focusMucJid = null;
79
-    this.bridgeIsDown = false;
78
+    this.noBridgeAvailable = false;
80 79
     this.options = options || {};
81 80
     this.moderator = new Moderator(this.roomjid, this.xmpp, this.eventEmitter,
82 81
         settings, {connection: this.xmpp.options, conference: this.options});
83 82
     this.initPresenceMap();
84 83
     this.session = null;
85
-    var self = this;
86 84
     this.lastPresences = {};
87 85
     this.phoneNumber = null;
88 86
     this.phonePin = null;
89 87
     this.connectionTimes = {};
90 88
     this.participantPropertyListener = null;
89
+
90
+    this.locked = false;
91 91
 }
92 92
 
93 93
 ChatRoom.prototype.initPresenceMap = function () {
@@ -121,8 +121,7 @@ ChatRoom.prototype.updateDeviceAvailability = function (devices) {
121 121
 };
122 122
 
123 123
 ChatRoom.prototype.join = function (password) {
124
-    if(password)
125
-        this.password = password;
124
+    this.password = password;
126 125
     var self = this;
127 126
     this.moderator.allocateConferenceFocus(function () {
128 127
         self.sendPresence(true);
@@ -137,13 +136,20 @@ ChatRoom.prototype.sendPresence = function (fromJoin) {
137 136
     }
138 137
 
139 138
     var pres = $pres({to: to });
140
-    pres.c('x', {xmlns: this.presMap['xns']});
141 139
 
142
-    if (this.password) {
143
-        pres.c('password').t(this.password).up();
144
-    }
140
+    // xep-0045 defines: "including in the initial presence stanza an empty
141
+    // <x/> element qualified by the 'http://jabber.org/protocol/muc' namespace"
142
+    // and subsequent presences should not include that or it can be considered
143
+    // as joining, and server can send us the message history for the room on
144
+    // every presence
145
+    if (fromJoin) {
146
+        pres.c('x', {xmlns: this.presMap['xns']});
145 147
 
146
-    pres.up();
148
+        if (this.password) {
149
+            pres.c('password').t(this.password).up();
150
+        }
151
+        pres.up();
152
+    }
147 153
 
148 154
     // Send XEP-0115 'c' stanza that contains our capabilities info
149 155
     var connection = this.connection;
@@ -164,7 +170,10 @@ ChatRoom.prototype.sendPresence = function (fromJoin) {
164 170
     }
165 171
 };
166 172
 
167
-
173
+/**
174
+ * Sends the presence unavailable, signaling the server
175
+ * we want to leave the room.
176
+ */
168 177
 ChatRoom.prototype.doLeave = function () {
169 178
     logger.log("do leave", this.myroomjid);
170 179
     var pres = $pres({to: this.myroomjid, type: 'unavailable' });
@@ -186,6 +195,25 @@ ChatRoom.prototype.doLeave = function () {
186 195
     this.connection.flush();
187 196
 };
188 197
 
198
+ChatRoom.prototype.discoRoomInfo = function () {
199
+  // https://xmpp.org/extensions/xep-0045.html#disco-roominfo
200
+
201
+  var getInfo = $iq({type: 'get', to: this.roomjid})
202
+    .c('query', {xmlns: Strophe.NS.DISCO_INFO});
203
+
204
+  this.connection.sendIQ(getInfo, function (result) {
205
+    var locked = $(result).find('>query>feature[var="muc_passwordprotected"]')
206
+        .length === 1;
207
+    if (locked != this.locked) {
208
+      this.eventEmitter.emit(XMPPEvents.MUC_LOCK_CHANGED, locked);
209
+      this.locked = locked;
210
+    }
211
+  }.bind(this), function (error) {
212
+    GlobalOnErrorHandler.callErrorHandler(error);
213
+    logger.error("Error getting room info: ", error);
214
+  }.bind(this));
215
+};
216
+
189 217
 
190 218
 ChatRoom.prototype.createNonAnonymousRoom = function () {
191 219
     // http://xmpp.org/extensions/xep-0045.html#createroom-reserved
@@ -207,7 +235,7 @@ ChatRoom.prototype.createNonAnonymousRoom = function () {
207 235
             return;
208 236
         }
209 237
 
210
-        var formSubmit = $iq({to: this.roomjid, type: 'set'})
238
+        var formSubmit = $iq({to: self.roomjid, type: 'set'})
211 239
             .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
212 240
 
213 241
         formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
@@ -243,22 +271,21 @@ ChatRoom.prototype.onPresence = function (pres) {
243 271
     member.jid = jid;
244 272
     member.isFocus
245 273
         = jid && jid.indexOf(this.moderator.getFocusUserJid() + "/") === 0;
246
-
247 274
     member.isHiddenDomain
248 275
         = jid && jid.indexOf("@") > 0
249 276
             && this.options.hiddenDomain
250
-                === jid.substring(jid.indexOf("@") + 1, jid.indexOf("/"))
277
+                === jid.substring(jid.indexOf("@") + 1, jid.indexOf("/"));
251 278
 
252 279
     $(pres).find(">x").remove();
253 280
     var nodes = [];
254 281
     parser.packet2JSON(pres, nodes);
255 282
     this.lastPresences[from] = nodes;
256
-    var jibri = null;
283
+    let jibri = null;
257 284
     // process nodes to extract data needed for MUC_JOINED and MUC_MEMBER_JOINED
258 285
     // events
259
-    for(var i = 0; i < nodes.length; i++)
286
+    for(let i = 0; i < nodes.length; i++)
260 287
     {
261
-        var node = nodes[i];
288
+        const node = nodes[i];
262 289
         switch(node.tagName)
263 290
         {
264 291
             case "nick":
@@ -271,8 +298,9 @@ ChatRoom.prototype.onPresence = function (pres) {
271 298
     }
272 299
 
273 300
     if (from == this.myroomjid) {
274
-        if (member.affiliation == 'owner' && this.role !== member.role) {
275
-            this.role = member.role;
301
+        var newRole = member.affiliation == "owner"? member.role : "none";
302
+        if (this.role !== newRole) {
303
+            this.role = newRole;
276 304
             this.eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED, this.role);
277 305
         }
278 306
         if (!this.joined) {
@@ -280,6 +308,11 @@ ChatRoom.prototype.onPresence = function (pres) {
280 308
             var now = this.connectionTimes["muc.joined"] =
281 309
                 window.performance.now();
282 310
             logger.log("(TIME) MUC joined:\t", now);
311
+
312
+            // set correct initial state of locked
313
+            if (this.password)
314
+                this.locked = true;
315
+
283 316
             this.eventEmitter.emit(XMPPEvents.MUC_JOINED);
284 317
         }
285 318
     } else if (this.members[from] === undefined) {
@@ -287,17 +320,8 @@ ChatRoom.prototype.onPresence = function (pres) {
287 320
         this.members[from] = member;
288 321
         logger.log('entered', from, member);
289 322
         if (member.isFocus) {
290
-            this.focusMucJid = from;
291
-            if(!this.recording) {
292
-                this.recording = new Recorder(this.options.recordingType,
293
-                    this.eventEmitter, this.connection, this.focusMucJid,
294
-                    this.options.jirecon, this.roomjid);
295
-                if(this.lastJibri)
296
-                    this.recording.handleJibriPresence(this.lastJibri);
297
-            }
298
-            logger.info("Ignore focus: " + from + ", real JID: " + jid);
299
-        }
300
-        else {
323
+            this._initFocus(from, jid);
324
+        } else {
301 325
             this.eventEmitter.emit(
302 326
                 XMPPEvents.MUC_MEMBER_JOINED,
303 327
                 from, member.nick, member.role, member.isHiddenDomain);
@@ -312,6 +336,19 @@ ChatRoom.prototype.onPresence = function (pres) {
312 336
                 XMPPEvents.MUC_ROLE_CHANGED, from, member.role);
313 337
         }
314 338
 
339
+        if (member.isFocus) {
340
+            // From time to time first few presences of the focus are not
341
+            // containing it's jid. That way we can mark later the focus member
342
+            // instead of not marking it at all and not starting the conference.
343
+            // FIXME: Maybe there is a better way to handle this issue. It seems
344
+            // there is some period of time in prosody that the configuration
345
+            // form is received but not applied. And if any participant joins
346
+            // during that period of time the first presence from the focus
347
+            // won't conain <item jid="focus..." />.
348
+            memberOfThis.isFocus = true;
349
+            this._initFocus(from, jid);
350
+        }
351
+
315 352
         // store the new display name
316 353
         if(member.displayName)
317 354
             memberOfThis.displayName = member.displayName;
@@ -319,9 +356,9 @@ ChatRoom.prototype.onPresence = function (pres) {
319 356
 
320 357
     // after we had fired member or room joined events, lets fire events
321 358
     // for the rest info we got in presence
322
-    for(var i = 0; i < nodes.length; i++)
359
+    for(let i = 0; i < nodes.length; i++)
323 360
     {
324
-        var node = nodes[i];
361
+        const node = nodes[i];
325 362
         switch(node.tagName)
326 363
         {
327 364
             case "nick":
@@ -335,14 +372,14 @@ ChatRoom.prototype.onPresence = function (pres) {
335 372
                     }
336 373
                 }
337 374
                 break;
338
-            case "bridgeIsDown":
339
-                if (member.isFocus && !this.bridgeIsDown) {
340
-                    this.bridgeIsDown = true;
375
+            case "bridgeNotAvailable":
376
+                if (member.isFocus && !this.noBridgeAvailable) {
377
+                    this.noBridgeAvailable = true;
341 378
                     this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
342 379
                 }
343 380
                 break;
344 381
             case "jibri-recording-status":
345
-                var jibri = node;
382
+                jibri = node;
346 383
                 break;
347 384
             case "call-control":
348 385
                 var att = node.attributes;
@@ -370,6 +407,23 @@ ChatRoom.prototype.onPresence = function (pres) {
370 407
     }
371 408
 };
372 409
 
410
+/**
411
+ * Initialize some properties when the focus participant is verified.
412
+ * @param from jid of the focus
413
+ * @param mucJid the jid of the focus in the muc
414
+ */
415
+ChatRoom.prototype._initFocus = function (from, mucJid) {
416
+    this.focusMucJid = from;
417
+    if(!this.recording) {
418
+        this.recording = new Recorder(this.options.recordingType,
419
+            this.eventEmitter, this.connection, this.focusMucJid,
420
+            this.options.jirecon, this.roomjid);
421
+        if(this.lastJibri)
422
+            this.recording.handleJibriPresence(this.lastJibri);
423
+    }
424
+    logger.info("Ignore focus: " + from + ", real JID: " + mucJid);
425
+};
426
+
373 427
 /**
374 428
  * Sets the special listener to be used for "command"s whose name starts with
375 429
  * "jitsi_participant_".
@@ -442,32 +496,44 @@ ChatRoom.prototype.onPresenceUnavailable = function (pres, from) {
442 496
             reason = reasonSelect.text();
443 497
         }
444 498
 
445
-        this.leave();
499
+        this._dispose();
446 500
 
447 501
         this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
448
-        delete this.connection.emuc.rooms[Strophe.getBareJidFromJid(from)];
502
+        this.connection.emuc.doLeave(this.roomjid);
449 503
         return true;
450 504
     }
451 505
 
452 506
     // Status code 110 indicates that this notification is "self-presence".
453
-    if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
507
+    var isSelfPresence = $(pres).find(
508
+            '>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]'
509
+        ).length !== 0;
510
+    var isKick = $(pres).find(
511
+            '>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]'
512
+        ).length !== 0;
513
+
514
+    if (!isSelfPresence) {
454 515
         delete this.members[from];
455 516
         this.onParticipantLeft(from, false);
456 517
     }
457 518
     // If the status code is 110 this means we're leaving and we would like
458 519
     // to remove everyone else from our view, so we trigger the event.
459
-    else if (Object.keys(this.members).length > 1) {
460
-        for (var i in this.members) {
461
-            var member = this.members[i];
520
+    else if (Object.keys(this.members).length > 0) {
521
+        for (const i in this.members) {
522
+            const member = this.members[i];
462 523
             delete this.members[i];
463 524
             this.onParticipantLeft(i, member.isFocus);
464 525
         }
526
+        this.connection.emuc.doLeave(this.roomjid);
527
+
528
+        // we fire muc_left only if this is not a kick,
529
+        // kick has both statuses 110 and 307.
530
+        if (!isKick)
531
+            this.eventEmitter.emit(XMPPEvents.MUC_LEFT);
465 532
     }
466
-    if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
467
-        if (this.myroomjid === from) {
468
-            this.leave();
469
-            this.eventEmitter.emit(XMPPEvents.KICKED);
470
-        }
533
+
534
+    if (isKick && this.myroomjid === from) {
535
+        this._dispose();
536
+        this.eventEmitter.emit(XMPPEvents.KICKED);
471 537
     }
472 538
 };
473 539
 
@@ -508,6 +574,10 @@ ChatRoom.prototype.onMessage = function (msg, from) {
508 574
         }
509 575
     }
510 576
 
577
+    if (from==this.roomjid && $(msg).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="104"]').length) {
578
+      this.discoRoomInfo();
579
+    }
580
+
511 581
     if (txt) {
512 582
         logger.log('chat', nick, txt);
513 583
         this.eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
@@ -527,19 +597,19 @@ ChatRoom.prototype.onPresenceError = function (pres, from) {
527 597
             // result in reconnection from authorized domain.
528 598
             // We're either missing Jicofo/Prosody config for anonymous
529 599
             // domains or something is wrong.
530
-            this.eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
600
+            this.eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR);
531 601
 
532 602
         } else {
533 603
             logger.warn('onPresError ', pres);
534
-            this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
604
+            this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR);
535 605
         }
536 606
     } else if($(pres).find('>error>service-unavailable').length) {
537 607
         logger.warn('Maximum users limit for the room has been reached',
538 608
             pres);
539
-        this.eventEmitter.emit(XMPPEvents.ROOM_MAX_USERS_ERROR, pres);
609
+        this.eventEmitter.emit(XMPPEvents.ROOM_MAX_USERS_ERROR);
540 610
     } else {
541 611
         logger.warn('onPresError ', pres);
542
-        this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
612
+        this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR);
543 613
     }
544 614
 };
545 615
 
@@ -605,7 +675,9 @@ ChatRoom.prototype.removePresenceListener = function (name) {
605 675
  * Checks if the user identified by given <tt>mucJid</tt> is the conference
606 676
  * focus.
607 677
  * @param mucJid the full MUC address of the user to be checked.
608
- * @returns {boolean} <tt>true</tt> if MUC user is the conference focus.
678
+ * @returns {boolean|null} <tt>true</tt> if MUC user is the conference focus or
679
+ * <tt>false</tt> if is not. When given <tt>mucJid</tt> does not exist in
680
+ * the MUC then <tt>null</tt> is returned.
609 681
  */
610 682
 ChatRoom.prototype.isFocus = function (mucJid) {
611 683
     var member = this.members[mucJid];
@@ -685,7 +757,7 @@ ChatRoom.prototype.generateNewStreamSSRCInfo = function () {
685 757
     return this.session.generateNewStreamSSRCInfo();
686 758
 };
687 759
 
688
-ChatRoom.prototype.setVideoMute = function (mute, callback, options) {
760
+ChatRoom.prototype.setVideoMute = function (mute, callback) {
689 761
     this.sendVideoInfoPresence(mute);
690 762
     if(callback)
691 763
         callback(mute);
@@ -746,6 +818,11 @@ ChatRoom.prototype.remoteTrackAdded = function(data) {
746 818
             mutedNode = filterNodeFromPresenceJSON(pres, "audiomuted");
747 819
         } else if (mediaType === MediaType.VIDEO) {
748 820
             mutedNode = filterNodeFromPresenceJSON(pres, "videomuted");
821
+            var videoTypeNode = filterNodeFromPresenceJSON(pres, "videoType");
822
+            if(videoTypeNode
823
+                && videoTypeNode.length > 0
824
+                && videoTypeNode[0])
825
+                data.videoType = videoTypeNode[0]["value"];
749 826
         } else {
750 827
             logger.warn("Unsupported media type: " + mediaType);
751 828
             data.muted = null;
@@ -776,14 +853,14 @@ ChatRoom.prototype.isRecordingSupported = function () {
776 853
  */
777 854
 ChatRoom.prototype.getRecordingState = function () {
778 855
     return (this.recording) ? this.recording.getState() : undefined;
779
-}
856
+};
780 857
 
781 858
 /**
782 859
  * Returns the url of the recorded video.
783 860
  */
784 861
 ChatRoom.prototype.getRecordingURL = function () {
785 862
     return (this.recording) ? this.recording.getURL() : null;
786
-}
863
+};
787 864
 
788 865
 /**
789 866
  * Starts/stops the recording
@@ -889,14 +966,38 @@ ChatRoom.prototype.onMute = function (iq) {
889 966
 
890 967
 /**
891 968
  * Leaves the room. Closes the jingle session.
969
+ * @returns {Promise} which is resolved if XMPPEvents.MUC_LEFT is received less
970
+ * than 5s after sending presence unavailable. Otherwise the promise is
971
+ * rejected.
892 972
  */
893 973
 ChatRoom.prototype.leave = function () {
974
+    this._dispose();
975
+    return new Promise((resolve, reject) => {
976
+        let timeout = setTimeout(() => onMucLeft(true), 5000);
977
+        let eventEmitter = this.eventEmitter;
978
+        function onMucLeft(doReject = false) {
979
+            eventEmitter.removeListener(XMPPEvents.MUC_LEFT, onMucLeft);
980
+            clearTimeout(timeout);
981
+            if(doReject) {
982
+                // the timeout expired
983
+                reject(new Error("The timeout for the confirmation about " +
984
+                    "leaving the room expired."));
985
+            } else {
986
+                resolve();
987
+            }
988
+        }
989
+        eventEmitter.on(XMPPEvents.MUC_LEFT, onMucLeft);
990
+        this.doLeave();
991
+    });
992
+};
993
+
994
+/**
995
+ * Disposes the conference, closes the jingle session.
996
+ */
997
+ChatRoom.prototype._dispose = function () {
894 998
     if (this.session) {
895 999
         this.session.close();
896 1000
     }
897
-    this.eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE);
898
-    this.doLeave();
899
-    this.connection.emuc.doLeave(this.roomjid);
900 1001
 };
901 1002
 
902 1003
 module.exports = ChatRoom;

+ 11
- 0
modules/xmpp/ConnectionPlugin.js View File

@@ -0,0 +1,11 @@
1
+/**
2
+ * Base class for strophe connection plugins.
3
+ */
4
+export default class ConnectionPlugin {
5
+    constructor() {
6
+        this.connection = null;
7
+    }
8
+    init (connection) {
9
+        this.connection = connection;
10
+    }
11
+}

+ 23
- 10
modules/xmpp/JingleSession.js View File

@@ -3,7 +3,10 @@
3 3
  * have different implementations depending on the underlying interface used
4 4
  * (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
5 5
  */
6
-var logger = require("jitsi-meet-logger").getLogger(__filename);
6
+import {getLogger} from "jitsi-meet-logger";
7
+const logger = getLogger(__filename);
8
+
9
+import * as JingleSessionState from "./JingleSessionState";
7 10
 
8 11
 function JingleSession(me, sid, peerjid, connection,
9 12
                        media_constraints, ice_config, service, eventEmitter) {
@@ -58,7 +61,10 @@ function JingleSession(me, sid, peerjid, connection,
58 61
     // The chat room instance associated with the session.
59 62
     this.room = null;
60 63
 
61
-    // Jingle session state - uninitialized until 'initialize' is called
64
+    /**
65
+     * Jingle session state - uninitialized until {@link initialize} is called
66
+     * @type {JingleSessionState}
67
+     */
62 68
     this.state = null;
63 69
 }
64 70
 
@@ -76,7 +82,7 @@ JingleSession.prototype.initialize = function(isInitiator, room) {
76 82
         throw new Error(errmsg);
77 83
     }
78 84
     this.room = room;
79
-    this.state = 'pending';
85
+    this.state = JingleSessionState.PENDING;
80 86
     this.initiator = isInitiator ? this.me : this.peerjid;
81 87
     this.responder = !isInitiator ? this.me : this.peerjid;
82 88
     this.doInitialize();
@@ -91,16 +97,15 @@ JingleSession.prototype.doInitialize = function() {};
91 97
  * Adds the ICE candidates found in the 'contents' array as remote candidates?
92 98
  * Note: currently only used on transport-info
93 99
  */
100
+// eslint-disable-next-line no-unused-vars
94 101
 JingleSession.prototype.addIceCandidates = function(contents) {};
95 102
 
96 103
 /**
97
- * Checks if this JingleSession is in 'active' state which means that the call
98
- * is in progress.
99
- * @returns {boolean} <tt>true</tt> if this JingleSession is in 'active' state
100
- *          or <tt>false</tt> otherwise.
104
+ * Returns current state of this <tt>JingleSession</tt> instance.
105
+ * @returns {JingleSessionState} the current state of this session instance.
101 106
  */
102
-JingleSession.prototype.active = function () {
103
-    return this.state === 'active';
107
+JingleSession.prototype.getState = function () {
108
+    return this.state;
104 109
 };
105 110
 
106 111
 /**
@@ -108,6 +113,7 @@ JingleSession.prototype.active = function () {
108 113
  *
109 114
  * @param contents an array of Jingle 'content' elements.
110 115
  */
116
+// eslint-disable-next-line no-unused-vars
111 117
 JingleSession.prototype.addSources = function(contents) {};
112 118
 
113 119
 /**
@@ -115,14 +121,20 @@ JingleSession.prototype.addSources = function(contents) {};
115 121
  *
116 122
  * @param contents an array of Jingle 'content' elements.
117 123
  */
124
+// eslint-disable-next-line no-unused-vars
118 125
 JingleSession.prototype.removeSources = function(contents) {};
119 126
 
120 127
 /**
121 128
  * Terminates this Jingle session by sending session-terminate
122 129
  * @param reason XMPP Jingle error condition
123 130
  * @param text some meaningful error message
131
+ * @param success a callback called once the 'session-terminate' packet has been
132
+ * acknowledged with RESULT.
133
+ * @param failure a callback called when either timeout occurs or ERROR response
134
+ * is received.
124 135
  */
125
-JingleSession.prototype.terminate = function(reason, text) {};
136
+// eslint-disable-next-line no-unused-vars
137
+JingleSession.prototype.terminate = function(reason, text, success, failure) {};
126 138
 
127 139
 /**
128 140
  * Handles an offer from the remote peer (prepares to accept a session).
@@ -132,6 +144,7 @@ JingleSession.prototype.terminate = function(reason, text) {};
132 144
  *        object with details(which is meant more to be printed to the logger
133 145
  *        than analysed in the code, as the error is unrecoverable anyway)
134 146
  */
147
+// eslint-disable-next-line no-unused-vars
135 148
 JingleSession.prototype.acceptOffer = function(jingle, success, failure) {};
136 149
 
137 150
 module.exports = JingleSession;

+ 78
- 29
modules/xmpp/JingleSessionPC.js View File

@@ -1,9 +1,9 @@
1
-/* jshint -W117 */
1
+/* global $, $iq */
2 2
 
3
-var logger = require("jitsi-meet-logger").getLogger(__filename);
3
+import {getLogger} from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
4 5
 var JingleSession = require("./JingleSession");
5 6
 var TraceablePeerConnection = require("./TraceablePeerConnection");
6
-var MediaType = require("../../service/RTC/MediaType");
7 7
 var SDPDiffer = require("./SDPDiffer");
8 8
 var SDPUtil = require("./SDPUtil");
9 9
 var SDP = require("./SDP");
@@ -12,6 +12,9 @@ var XMPPEvents = require("../../service/xmpp/XMPPEvents");
12 12
 var RTCBrowserType = require("../RTC/RTCBrowserType");
13 13
 var RTC = require("../RTC/RTC");
14 14
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
15
+var Statistics = require("../statistics/statistics");
16
+
17
+import * as JingleSessionState from "./JingleSessionState";
15 18
 
16 19
 /**
17 20
  * Constant tells how long we're going to wait for IQ response, before timeout
@@ -58,6 +61,12 @@ function JingleSessionPC(me, sid, peerjid, connection,
58 61
     this.jingleOfferIq = null;
59 62
     this.webrtcIceUdpDisable = !!this.service.options.webrtcIceUdpDisable;
60 63
     this.webrtcIceTcpDisable = !!this.service.options.webrtcIceTcpDisable;
64
+    /**
65
+     * Flag used to enforce ICE failure through the URL parameter for
66
+     * the automatic testing purpose.
67
+     * @type {boolean}
68
+     */
69
+    this.failICE = !!this.service.options.failICE;
61 70
 
62 71
     this.modifySourcesQueue = async.queue(this._modifySources.bind(this), 1);
63 72
 }
@@ -95,7 +104,7 @@ JingleSessionPC.prototype.doInitialize = function () {
95 104
             var protocol = candidate.protocol;
96 105
             if (typeof protocol === 'string') {
97 106
                 protocol = protocol.toLowerCase();
98
-                if (protocol == 'tcp') {
107
+                if (protocol === 'tcp' || protocol ==='ssltcp') {
99 108
                     if (self.webrtcIceTcpDisable)
100 109
                         return;
101 110
                 } else if (protocol == 'udp') {
@@ -112,26 +121,30 @@ JingleSessionPC.prototype.doInitialize = function () {
112 121
     this.peerconnection.onremovestream = function (event) {
113 122
         self.remoteStreamRemoved(event.stream);
114 123
     };
115
-    this.peerconnection.onsignalingstatechange = function (event) {
124
+    this.peerconnection.onsignalingstatechange = function () {
116 125
         if (!(self && self.peerconnection)) return;
117 126
         if (self.peerconnection.signalingState === 'stable') {
118 127
             self.wasstable = true;
119 128
         }
120 129
     };
121 130
     /**
122
-     * The oniceconnectionstatechange event handler contains the code to execute when the iceconnectionstatechange event,
123
-     * of type Event, is received by this RTCPeerConnection. Such an event is sent when the value of
131
+     * The oniceconnectionstatechange event handler contains the code to execute
132
+     * when the iceconnectionstatechange event, of type Event, is received by
133
+     * this RTCPeerConnection. Such an event is sent when the value of
124 134
      * RTCPeerConnection.iceConnectionState changes.
125
-     *
126
-     * @param event the event containing information about the change
127 135
      */
128
-    this.peerconnection.oniceconnectionstatechange = function (event) {
136
+    this.peerconnection.oniceconnectionstatechange = function () {
129 137
         if (!(self && self.peerconnection)) return;
130 138
         var now = window.performance.now();
131 139
         self.room.connectionTimes["ice.state." +
132 140
             self.peerconnection.iceConnectionState] = now;
133 141
         logger.log("(TIME) ICE " + self.peerconnection.iceConnectionState +
134 142
                     ":\t", now);
143
+        Statistics.analytics.sendEvent(
144
+            'ice.' + self.peerconnection.iceConnectionState, {value: now});
145
+        self.room.eventEmitter.emit(
146
+            XMPPEvents.ICE_CONNECTION_STATE_CHANGED,
147
+            self.peerconnection.iceConnectionState);
135 148
         switch (self.peerconnection.iceConnectionState) {
136 149
             case 'connected':
137 150
 
@@ -155,7 +168,7 @@ JingleSessionPC.prototype.doInitialize = function () {
155 168
                 break;
156 169
         }
157 170
     };
158
-    this.peerconnection.onnegotiationneeded = function (event) {
171
+    this.peerconnection.onnegotiationneeded = function () {
159 172
         self.room.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, self);
160 173
     };
161 174
 };
@@ -210,7 +223,12 @@ JingleSessionPC.prototype.sendIceCandidates = function (candidates) {
210 223
                 name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
211 224
             }).c('transport', ice);
212 225
             for (var i = 0; i < cands.length; i++) {
213
-                cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
226
+                var candidate = SDPUtil.candidateToJingle(cands[i].candidate);
227
+                // Mangle ICE candidate if 'failICE' test option is enabled
228
+                if (this.service.options.failICE) {
229
+                    candidate.ip = "1.1.1.1";
230
+                }
231
+                cand.c('candidate', candidate).up();
214 232
             }
215 233
             // add fingerprint
216 234
             var fingerprint_line = SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session);
@@ -241,8 +259,6 @@ JingleSessionPC.prototype.sendIceCandidates = function (candidates) {
241 259
 JingleSessionPC.prototype.readSsrcInfo = function (contents) {
242 260
     var self = this;
243 261
     $(contents).each(function (idx, content) {
244
-        var name = $(content).attr('name');
245
-        var mediaType = this.getAttribute('name');
246 262
         var ssrcs = $(content).find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
247 263
         ssrcs.each(function () {
248 264
             var ssrc = this.getAttribute('ssrc');
@@ -269,7 +285,7 @@ JingleSessionPC.prototype.readSsrcInfo = function (contents) {
269 285
  */
270 286
 JingleSessionPC.prototype.acceptOffer = function(jingleOffer,
271 287
                                                  success, failure) {
272
-    this.state = 'active';
288
+    this.state = JingleSessionState.ACTIVE;
273 289
     this.setOfferCycle(jingleOffer,
274 290
         function() {
275 291
             // setOfferCycle succeeded, now we have self.localSDP up to date
@@ -423,6 +439,9 @@ JingleSessionPC.prototype.sendSessionAccept = function (localSDP,
423 439
     if (this.webrtcIceUdpDisable) {
424 440
         localSDP.removeUdpCandidates = true;
425 441
     }
442
+    if (this.failICE) {
443
+        localSDP.failICE = true;
444
+    }
426 445
     localSDP.toJingle(
427 446
         accept,
428 447
         this.initiator == this.me ? 'initiator' : 'responder',
@@ -432,15 +451,34 @@ JingleSessionPC.prototype.sendSessionAccept = function (localSDP,
432 451
     // Calling tree() to print something useful
433 452
     accept = accept.tree();
434 453
     logger.info("Sending session-accept", accept);
435
-
454
+    var self = this;
436 455
     this.connection.sendIQ(accept,
437 456
         success,
438
-        this.newJingleErrorHandler(accept, failure),
457
+        this.newJingleErrorHandler(accept, function (error) {
458
+            failure(error);
459
+            // 'session-accept' is a critical timeout and we'll have to restart
460
+            self.room.eventEmitter.emit(XMPPEvents.SESSION_ACCEPT_TIMEOUT);
461
+        }),
439 462
         IQ_TIMEOUT);
440 463
     // XXX Videobridge needs WebRTC's answer (ICE ufrag and pwd, DTLS
441 464
     // fingerprint and setup) ASAP in order to start the connection
442 465
     // establishment.
443
-    this.connection.flush();
466
+    //
467
+    // FIXME Flushing the connection at this point triggers an issue with BOSH
468
+    // request handling in Prosody on slow connections.
469
+    //
470
+    // The problem is that this request will be quite large and it may take time
471
+    // before it reaches Prosody. In the meantime Strophe may decide to send
472
+    // the next one. And it was observed that a small request with
473
+    // 'transport-info' usually follows this one. It does reach Prosody before
474
+    // the previous one was completely received. 'rid' on the server is
475
+    // increased and Prosody ignores the request with 'session-accept'. It will
476
+    // never reach Jicofo and everything in the request table is lost. Removing
477
+    // the flush does not guarantee it will never happen, but makes it much less
478
+    // likely('transport-info' is bundled with 'session-accept' and any
479
+    // immediate requests).
480
+    //
481
+    // this.connection.flush();
444 482
 };
445 483
 
446 484
 /**
@@ -508,9 +546,13 @@ JingleSessionPC.prototype.sendTransportReject = function(success, failure) {
508 546
         IQ_TIMEOUT);
509 547
 };
510 548
 
511
-//FIXME: I think this method is not used!
549
+/**
550
+ * @inheritDoc
551
+ */
512 552
 JingleSessionPC.prototype.terminate = function (reason,  text,
513 553
                                                 success, failure) {
554
+    this.state = JingleSessionState.ENDED;
555
+
514 556
     var term = $iq({to: this.peerjid,
515 557
         type: 'set'})
516 558
         .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
@@ -556,7 +598,7 @@ JingleSessionPC.prototype.addSource = function (elem) {
556 598
     // FIXME: dirty waiting
557 599
     if (!this.peerconnection.localDescription)
558 600
     {
559
-        logger.warn("addSource - localDescription not ready yet")
601
+        logger.warn("addSource - localDescription not ready yet");
560 602
         setTimeout(function()
561 603
             {
562 604
                 self.addSource(elem);
@@ -736,6 +778,9 @@ JingleSessionPC.prototype._modifySources = function (successCallback, queueCallb
736 778
         if (this.webrtcIceUdpDisable) {
737 779
             sdp.removeUdpCandidates = true;
738 780
         }
781
+        if (this.failICE) {
782
+            sdp.failICE = true;
783
+        }
739 784
 
740 785
         sdp.fromJingle(this.jingleOfferIq);
741 786
         this.readSsrcInfo($(this.jingleOfferIq).find(">content"));
@@ -765,12 +810,13 @@ JingleSessionPC.prototype._modifySources = function (successCallback, queueCallb
765 810
     this.removessrc = [];
766 811
 
767 812
     sdp.raw = sdp.session + sdp.media.join('');
813
+
768 814
     /**
769 815
      * Implements a failure callback which reports an error message and an
770 816
      * optional error through (1) logger, (2) GlobalOnErrorHandler, and (3)
771 817
      * queueCallback.
772 818
      *
773
-     * @param {string} errmsg the error messsage to report
819
+     * @param {string} errmsg the error message to report
774 820
      * @param {*} error an optional error to report in addition to errmsg
775 821
      */
776 822
     function reportError(errmsg, err) {
@@ -783,7 +829,7 @@ JingleSessionPC.prototype._modifySources = function (successCallback, queueCallb
783 829
         }
784 830
         GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
785 831
         queueCallback(err);
786
-    };
832
+    }
787 833
 
788 834
     var ufrag = getUfrag(sdp.raw);
789 835
     if (ufrag != self.remoteUfrag) {
@@ -893,7 +939,7 @@ JingleSessionPC.prototype.addStream = function (stream, callback, errorCallback,
893 939
             errorCallback(error);
894 940
         }
895 941
     });
896
-}
942
+};
897 943
 
898 944
 /**
899 945
  * Generate ssrc info object for a stream with the following properties:
@@ -997,7 +1043,7 @@ JingleSessionPC.prototype.removeStream = function (stream, callback, errorCallba
997 1043
             errorCallback(error);
998 1044
         }
999 1045
     });
1000
-}
1046
+};
1001 1047
 
1002 1048
 /**
1003 1049
  * Figures out added/removed ssrcs and send update IQs.
@@ -1006,9 +1052,9 @@ JingleSessionPC.prototype.removeStream = function (stream, callback, errorCallba
1006 1052
  */
1007 1053
 JingleSessionPC.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
1008 1054
 
1009
-    if (!(this.peerconnection.signalingState == 'stable' &&
1010
-        this.peerconnection.iceConnectionState == 'connected')){
1011
-        logger.log("Too early to send updates");
1055
+    if (this.state !== JingleSessionState.ACTIVE){
1056
+        logger.warn(
1057
+            "Skipping SSRC update in \'" + this.state + " \' state.");
1012 1058
         return;
1013 1059
     }
1014 1060
 
@@ -1103,7 +1149,10 @@ JingleSessionPC.prototype.newJingleErrorHandler = function(request, failureCb) {
1103 1149
             error.source = request.tree();
1104 1150
         }
1105 1151
 
1106
-        error.session = this;
1152
+        // Commented to fix JSON.stringify(error) exception for circular
1153
+        // dependancies when we print that error.
1154
+        // FIXME: Maybe we can include part of the session object
1155
+        // error.session = this;
1107 1156
 
1108 1157
         logger.error("Jingle error", error);
1109 1158
         if (failureCb) {
@@ -1459,7 +1508,7 @@ function getUfrag(sdp) {
1459 1508
     var ufragLines = sdp.split('\n').filter(function(line) {
1460 1509
         return line.startsWith("a=ice-ufrag:");});
1461 1510
     if (ufragLines.length > 0) {
1462
-        return ufragLines[0].substr("a=ice-ufrag:".length)
1511
+        return ufragLines[0].substr("a=ice-ufrag:".length);
1463 1512
     }
1464 1513
 }
1465 1514
 

+ 22
- 0
modules/xmpp/JingleSessionState.js View File

@@ -0,0 +1,22 @@
1
+/**
2
+ * The pending Jingle session state which means the session as defined in
3
+ * XEP-0166(before 'session-invite/session-accept' took place).
4
+ *
5
+ * @type {string}
6
+ */
7
+export const PENDING = 'pending';
8
+
9
+/**
10
+ * The active Jingle session state as defined in XEP-0166
11
+ * (after 'session-invite'/'session-accept').
12
+ *
13
+ * @type {string}
14
+ */
15
+export const ACTIVE = 'active';
16
+
17
+/**
18
+ * The ended Jingle session state as defined in XEP-0166
19
+ * (after 'session-terminate').
20
+ * @type {string}
21
+ */
22
+export const ENDED = 'ended';

+ 21
- 10
modules/xmpp/SDP.js View File

@@ -1,6 +1,5 @@
1
-/* jshint -W117 */
1
+/* global $, APP */
2 2
 
3
-var logger = require("jitsi-meet-logger").getLogger(__filename);
4 3
 var SDPUtil = require("./SDPUtil");
5 4
 
6 5
 // SDP STUFF
@@ -20,6 +19,14 @@ function SDP(sdp) {
20 19
     this.session = session;
21 20
 }
22 21
 
22
+/**
23
+ * A flag will make {@link transportToJingle} and {@link jingle2media} replace
24
+ * ICE candidates IPs with invalid value of '1.1.1.1' which will cause ICE
25
+ * failure. The flag is used in the automated testing.
26
+ * @type {boolean}
27
+ */
28
+SDP.prototype.failICE = false;
29
+
23 30
 /**
24 31
  * Whether or not to remove TCP ice candidates when translating from/to jingle.
25 32
  * @type {boolean}
@@ -128,7 +135,7 @@ SDP.prototype.removeSessionLines = function(prefix) {
128 135
     });
129 136
     this.raw = this.session + this.media.join('');
130 137
     return lines;
131
-}
138
+};
132 139
 // remove lines matching prefix from a media section specified by mediaindex
133 140
 // TODO: non-numeric mediaindex could match mid
134 141
 SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
@@ -139,12 +146,10 @@ SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
139 146
     });
140 147
     this.raw = this.session + this.media.join('');
141 148
     return lines;
142
-}
149
+};
143 150
 
144 151
 // add content's to a jingle element
145 152
 SDP.prototype.toJingle = function (elem, thecreator) {
146
-//    logger.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]);
147
-    var self = this;
148 153
     var i, j, k, mline, ssrc, rtpmap, tmp, lines;
149 154
     // new bundle plan
150 155
     lines = SDPUtil.find_lines(this.session, 'a=group:');
@@ -388,10 +393,14 @@ SDP.prototype.transportToJingle = function (mediaindex, elem) {
388 393
             var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
389 394
             lines.forEach(function (line) {
390 395
                 var candidate = SDPUtil.candidateToJingle(line);
396
+                if (self.failICE) {
397
+                    candidate.ip = "1.1.1.1";
398
+                }
391 399
                 var protocol = (candidate &&
392 400
                         typeof candidate.protocol === 'string')
393 401
                     ? candidate.protocol.toLowerCase() : '';
394
-                if ((self.removeTcpCandidates && protocol === 'tcp') ||
402
+                if ((self.removeTcpCandidates
403
+                        && (protocol === 'tcp' || protocol === 'ssltcp')) ||
395 404
                     (self.removeUdpCandidates && protocol === 'udp')) {
396 405
                     return;
397 406
                 }
@@ -400,7 +409,7 @@ SDP.prototype.transportToJingle = function (mediaindex, elem) {
400 409
         }
401 410
     }
402 411
     elem.up(); // end of transport
403
-}
412
+};
404 413
 
405 414
 SDP.prototype.rtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
406 415
     var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
@@ -482,7 +491,6 @@ SDP.prototype.fromJingle = function (jingle) {
482 491
 SDP.prototype.jingle2media = function (content) {
483 492
     var media = '',
484 493
         desc = content.find('description'),
485
-        ssrc = desc.attr('ssrc'),
486 494
         self = this,
487 495
         tmp;
488 496
     var sctp = content.find(
@@ -599,9 +607,12 @@ SDP.prototype.jingle2media = function (content) {
599 607
         var protocol = this.getAttribute('protocol');
600 608
         protocol = (typeof protocol === 'string') ? protocol.toLowerCase(): '';
601 609
 
602
-        if ((self.removeTcpCandidates && protocol === 'tcp') ||
610
+        if ((self.removeTcpCandidates
611
+                && (protocol === 'tcp' || protocol === 'ssltcp')) ||
603 612
             (self.removeUdpCandidates && protocol === 'udp')) {
604 613
             return;
614
+        } else  if (self.failICE) {
615
+            this.setAttribute('ip', '1.1.1.1');
605 616
         }
606 617
 
607 618
         media += SDPUtil.candidateFromJingle(this);

+ 3
- 2
modules/xmpp/SDPUtil.js View File

@@ -1,8 +1,9 @@
1
-var logger = require("jitsi-meet-logger").getLogger(__filename);
1
+import {getLogger} from "jitsi-meet-logger";
2
+const logger = getLogger(__filename);
2 3
 var RTCBrowserType = require("../RTC/RTCBrowserType");
3 4
 
4 5
 
5
-SDPUtil = {
6
+var SDPUtil = {
6 7
     filter_special_chars: function (text) {
7 8
         // XXX Neither one of the falsy values (e.g. null, undefined, false,
8 9
         // "", etc.) "contain" special chars.

+ 52
- 34
modules/xmpp/TraceablePeerConnection.js View File

@@ -1,6 +1,8 @@
1
-/* global $ */
1
+/* global mozRTCPeerConnection, webkitRTCPeerConnection */
2
+
3
+import { getLogger } from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
2 5
 var RTC = require('../RTC/RTC');
3
-var logger = require("jitsi-meet-logger").getLogger(__filename);
4 6
 var RTCBrowserType = require("../RTC/RTCBrowserType.js");
5 7
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
6 8
 var transform = require('sdp-transform');
@@ -186,7 +188,7 @@ TraceablePeerConnection.prototype.ssrcReplacement = function (desc) {
186 188
             var ssrcOperation = SSRCs[0];
187 189
             switch(ssrcOperation.type) {
188 190
                 case "mute":
189
-                case "addMuted":
191
+                case "addMuted": {
190 192
                 //FIXME: If we want to support multiple streams we have to add
191 193
                 // recv-only ssrcs for the
192 194
                 // muted streams on every change until the stream is unmuted
@@ -194,8 +196,8 @@ TraceablePeerConnection.prototype.ssrcReplacement = function (desc) {
194 196
                 // in the SDP
195 197
                     if(!bLine.ssrcs)
196 198
                         bLine.ssrcs = [];
197
-                    var groups = ssrcOperation.ssrc.groups;
198
-                    var ssrc = null;
199
+                    const groups = ssrcOperation.ssrc.groups;
200
+                    let ssrc = null;
199 201
                     if(groups && groups.length) {
200 202
                         ssrc = groups[0].primarySSRC;
201 203
                     } else if(ssrcOperation.ssrc.ssrcs &&
@@ -218,14 +220,15 @@ TraceablePeerConnection.prototype.ssrcReplacement = function (desc) {
218 220
                     // only 1 video stream that is muted.
219 221
                     this.recvOnlySSRCs[bLine.type] = ssrc;
220 222
                     break;
221
-                case "unmute":
223
+                }
224
+                case "unmute": {
222 225
                     if(!ssrcOperation.ssrc || !ssrcOperation.ssrc.ssrcs ||
223 226
                         !ssrcOperation.ssrc.ssrcs.length)
224 227
                         break;
225 228
                     var ssrcMap = {};
226 229
                     var ssrcLastIdx = ssrcOperation.ssrc.ssrcs.length - 1;
227 230
                     for(var i = 0; i < bLine.ssrcs.length; i++) {
228
-                        var ssrc = bLine.ssrcs[i];
231
+                        const ssrc = bLine.ssrcs[i];
229 232
                         if (ssrc.attribute !== 'msid' &&
230 233
                             ssrc.value !== ssrcOperation.msid) {
231 234
                             continue;
@@ -236,7 +239,7 @@ TraceablePeerConnection.prototype.ssrcReplacement = function (desc) {
236 239
                         if(ssrcLastIdx < 0)
237 240
                             break;
238 241
                     }
239
-                    var groups = ssrcOperation.ssrc.groups;
242
+                    const groups = ssrcOperation.ssrc.groups;
240 243
                     if (typeof bLine.ssrcGroups !== 'undefined' &&
241 244
                         Array.isArray(bLine.ssrcGroups) && groups &&
242 245
                         groups.length) {
@@ -277,8 +280,9 @@ TraceablePeerConnection.prototype.ssrcReplacement = function (desc) {
277 280
                     // Storing the unmuted SSRCs.
278 281
                     permSSRCs.push(ssrcOperation);
279 282
                     break;
283
+                }
280 284
                 default:
281
-                break;
285
+                    break;
282 286
             }
283 287
             SSRCs = this.replaceSSRCs[bLine.type].splice(0,1);
284 288
         }
@@ -287,7 +291,7 @@ TraceablePeerConnection.prototype.ssrcReplacement = function (desc) {
287 291
 
288 292
         if (!Array.isArray(bLine.ssrcs) || bLine.ssrcs.length === 0)
289 293
         {
290
-            var ssrc = this.recvOnlySSRCs[bLine.type]
294
+            const ssrc = this.recvOnlySSRCs[bLine.type]
291 295
                 = this.recvOnlySSRCs[bLine.type] ||
292 296
                     RandomUtil.randomInt(1, 0xffffffff);
293 297
             bLine.ssrcs = [{
@@ -488,7 +492,7 @@ ssrcInfo) {
488 492
         // Removing all cached ssrcs for the streams that are removed or
489 493
         // muted.
490 494
         if(ssrcInfo && this.replaceSSRCs[ssrcInfo.mtype]) {
491
-            for(i = 0; i < this.replaceSSRCs[ssrcInfo.mtype].length; i++) {
495
+            for(var i = 0; i < this.replaceSSRCs[ssrcInfo.mtype].length; i++) {
492 496
                 var op = this.replaceSSRCs[ssrcInfo.mtype][i];
493 497
                 if(op.type === "unmute" &&
494 498
                     op.ssrc.ssrcs.join("_") ===
@@ -584,32 +588,45 @@ TraceablePeerConnection.prototype.createAnswer
584 588
     this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
585 589
     this.peerconnection.createAnswer(
586 590
         function (answer) {
587
-            self.trace('createAnswerOnSuccess::preTransform', dumpSDP(answer));
588
-            // if we're running on FF, transform to Plan A first.
589
-            if (RTCBrowserType.usesUnifiedPlan()) {
590
-                answer = self.interop.toPlanB(answer);
591
-                self.trace('createAnswerOnSuccess::postTransform (Plan B)',
592
-                    dumpSDP(answer));
593
-            }
594
-
595
-            if (!self.session.room.options.disableSimulcast
596
-                && self.simulcast.isSupported()) {
597
-                answer = self.simulcast.mungeLocalDescription(answer);
598
-                self.trace('createAnswerOnSuccess::postTransform (simulcast)',
599
-                    dumpSDP(answer));
600
-            }
591
+            try {
592
+                self.trace(
593
+                    'createAnswerOnSuccess::preTransform', dumpSDP(answer));
594
+                // if we're running on FF, transform to Plan A first.
595
+                if (RTCBrowserType.usesUnifiedPlan()) {
596
+                    answer = self.interop.toPlanB(answer);
597
+                    self.trace('createAnswerOnSuccess::postTransform (Plan B)',
598
+                        dumpSDP(answer));
599
+                }
601 600
 
602
-            if (!RTCBrowserType.isFirefox())
603
-            {
604
-                answer = self.ssrcReplacement(answer);
605
-                self.trace('createAnswerOnSuccess::mungeLocalVideoSSRC',
606
-                    dumpSDP(answer));
607
-            }
601
+                if (!self.session.room.options.disableSimulcast
602
+                    && self.simulcast.isSupported()) {
603
+                    answer = self.simulcast.mungeLocalDescription(answer);
604
+                    self.trace(
605
+                        'createAnswerOnSuccess::postTransform (simulcast)',
606
+                        dumpSDP(answer));
607
+                }
608 608
 
609
-            self.eventEmitter.emit(XMPPEvents.SENDRECV_STREAMS_CHANGED,
610
-                extractSSRCMap(answer));
609
+                if (!RTCBrowserType.isFirefox())
610
+                {
611
+                    answer = self.ssrcReplacement(answer);
612
+                    self.trace('createAnswerOnSuccess::mungeLocalVideoSSRC',
613
+                        dumpSDP(answer));
614
+                }
611 615
 
612
-            successCallback(answer);
616
+                self.eventEmitter.emit(XMPPEvents.SENDRECV_STREAMS_CHANGED,
617
+                    extractSSRCMap(answer));
618
+
619
+                successCallback(answer);
620
+            } catch (e) {
621
+                // there can be error modifying the answer, for example
622
+                // for ssrcReplacement there was a track with ssrc that is null
623
+                // and if we do not catch the error no callback is called
624
+                // at all
625
+                self.trace('createAnswerOnError', e);
626
+                self.trace('createAnswerOnError', dumpSDP(answer));
627
+                logger.error('createAnswerOnError', e, dumpSDP(answer));
628
+                failureCallback(e);
629
+            }
613 630
         },
614 631
         function(err) {
615 632
             self.trace('createAnswerOnFailure', err);
@@ -622,6 +639,7 @@ TraceablePeerConnection.prototype.createAnswer
622 639
 };
623 640
 
624 641
 TraceablePeerConnection.prototype.addIceCandidate
642
+        // eslint-disable-next-line no-unused-vars
625 643
         = function (candidate, successCallback, failureCallback) {
626 644
     //var self = this;
627 645
     this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));

+ 19
- 7
modules/xmpp/moderator.js View File

@@ -174,13 +174,11 @@ Moderator.prototype.createConferenceIq =  function () {
174 174
                 value: true
175 175
             }).up();
176 176
     //}
177
-    if (this.options.conference.enableLipSync !== undefined) {
178
-        elem.c(
179
-            'property', {
180
-                name: 'enableLipSync',
181
-                value: this.options.conference.enableLipSync
182
-            }).up();
183
-    }
177
+    elem.c(
178
+        'property', {
179
+            name: 'enableLipSync',
180
+            value: false !== this.options.connection.enableLipSync
181
+        }).up();
184 182
     if (this.options.conference.audioPacketDelay !== undefined) {
185 183
         elem.c(
186 184
             'property', {
@@ -236,6 +234,14 @@ Moderator.prototype.createConferenceIq =  function () {
236 234
             name: 'simulcastMode',
237 235
             value: 'rewriting'
238 236
         }).up();
237
+
238
+    if (this.options.conference.useRoomAsSharedDocumentName !== undefined) {
239
+        elem.c(
240
+            'property', {
241
+                name: 'useRoomAsSharedDocumentName',
242
+                value: this.options.conference.useRoomAsSharedDocumentName
243
+            }).up();
244
+    }
239 245
     elem.up();
240 246
     return elem;
241 247
 };
@@ -369,6 +375,12 @@ Moderator.prototype._allocateConferenceFocusError = function (error, callback) {
369 375
                 });
370 376
         return;
371 377
     }
378
+    if(this.retries >= this.maxRetries) {
379
+        self.eventEmitter.emit(
380
+                XMPPEvents.ALLOCATE_FOCUS_MAX_RETRIES_ERROR);
381
+        return;
382
+    }
383
+    this.retries++;
372 384
     var waitMs = self.getNextErrorTimeout();
373 385
     var errmsg = "Focus error, retry after "+ waitMs;
374 386
     GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));

+ 5
- 4
modules/xmpp/recording.js View File

@@ -1,10 +1,11 @@
1
-/* global $, $iq, config, connection, focusMucJid, messageHandler,
2
-   Toolbar, Util, Promise */
1
+/* global $, $iq */
2
+
3
+import { getLogger } from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
3 5
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
4 6
 var JitsiRecorderErrors = require("../../JitsiRecorderErrors");
5 7
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
6 8
 
7
-var logger = require("jitsi-meet-logger").getLogger(__filename);
8 9
 
9 10
 function Recording(type, eventEmitter, connection, focusMucJid, jirecon,
10 11
     roomjid) {
@@ -115,7 +116,7 @@ Recording.prototype.setRecordingJibri
115 116
 };
116 117
 
117 118
 Recording.prototype.setRecordingJirecon =
118
-    function (state, callback, errCallback, options) {
119
+    function (state, callback, errCallback) {
119 120
 
120 121
     if (state == this.state){
121 122
         errCallback(new Error("Invalid state!"));

+ 114
- 99
modules/xmpp/strophe.emuc.js View File

@@ -1,110 +1,125 @@
1
-/* jshint -W117 */
2 1
 /* a simple MUC connection plugin
3 2
  * can only handle a single MUC room
4 3
  */
5 4
 
6
-var logger = require("jitsi-meet-logger").getLogger(__filename);
7
-var ChatRoom = require("./ChatRoom");
8
-
9
-module.exports = function(XMPP) {
10
-    Strophe.addConnectionPlugin('emuc', {
11
-        connection: null,
12
-        rooms: {},//map with the rooms
13
-        init: function (conn) {
14
-            this.connection = conn;
15
-            // add handlers (just once)
16
-            this.connection.addHandler(this.onPresence.bind(this), null,
17
-                'presence', null, null, null, null);
18
-            this.connection.addHandler(this.onPresenceUnavailable.bind(this),
19
-                null, 'presence', 'unavailable', null);
20
-            this.connection.addHandler(this.onPresenceError.bind(this), null,
21
-                'presence', 'error', null);
22
-            this.connection.addHandler(this.onMessage.bind(this), null,
23
-                'message', null, null);
24
-            this.connection.addHandler(this.onMute.bind(this),
25
-                'http://jitsi.org/jitmeet/audio', 'iq', 'set',null,null);
26
-        },
27
-        createRoom: function (jid, password, options, settings) {
28
-            var roomJid = Strophe.getBareJidFromJid(jid);
29
-            if (this.rooms[roomJid]) {
30
-                var errmsg = "You are already in the room!";
31
-                logger.error(errmsg);
32
-                throw new Error(errmsg);
33
-                return;
34
-            }
35
-            this.rooms[roomJid] = new ChatRoom(this.connection, jid,
36
-                password, XMPP, options, settings);
37
-            return this.rooms[roomJid];
38
-        },
39
-        doLeave: function (jid) {
40
-            delete this.rooms[jid];
41
-        },
42
-        onPresence: function (pres) {
43
-            var from = pres.getAttribute('from');
44
-
45
-            // What is this for? A workaround for something?
46
-            if (pres.getAttribute('type')) {
47
-                return true;
48
-            }
49
-
50
-            var room = this.rooms[Strophe.getBareJidFromJid(from)];
51
-            if(!room)
52
-                return;
53
-
54
-            // Parse status.
55
-            if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
56
-                room.createNonAnonymousRoom();
57
-            }
58
-
59
-            room.onPresence(pres);
5
+/* global $, Strophe */
60 6
 
61
-            return true;
62
-        },
63
-        onPresenceUnavailable: function (pres) {
64
-            var from = pres.getAttribute('from');
65
-            var room = this.rooms[Strophe.getBareJidFromJid(from)];
66
-            if(!room)
67
-                return;
68
-
69
-            room.onPresenceUnavailable(pres, from);
70
-            return true;
71
-        },
72
-        onPresenceError: function (pres) {
73
-            var from = pres.getAttribute('from');
74
-            var room = this.rooms[Strophe.getBareJidFromJid(from)];
75
-            if(!room)
76
-                return;
77
-
78
-            room.onPresenceError(pres, from);
79
-            return true;
80
-        },
81
-        onMessage: function (msg) {
82
-            // FIXME: this is a hack. but jingle on muc makes nickchanges hard
83
-            var from = msg.getAttribute('from');
84
-            var room = this.rooms[Strophe.getBareJidFromJid(from)];
85
-            if(!room)
86
-                return;
87
-
88
-            room.onMessage(msg, from);
89
-            return true;
90
-        },
7
+import {getLogger} from "jitsi-meet-logger";
8
+const logger = getLogger(__filename);
9
+import ChatRoom from "./ChatRoom";
10
+import ConnectionPlugin from "./ConnectionPlugin";
11
+
12
+class MucConnectionPlugin extends ConnectionPlugin {
13
+    constructor(xmpp) {
14
+        super();
15
+        this.xmpp = xmpp;
16
+        this.rooms = {};
17
+    }
18
+
19
+    init (connection) {
20
+        super.init(connection);
21
+        // add handlers (just once)
22
+        this.connection.addHandler(this.onPresence.bind(this), null,
23
+            'presence', null, null, null, null);
24
+        this.connection.addHandler(this.onPresenceUnavailable.bind(this),
25
+            null, 'presence', 'unavailable', null);
26
+        this.connection.addHandler(this.onPresenceError.bind(this), null,
27
+            'presence', 'error', null);
28
+        this.connection.addHandler(this.onMessage.bind(this), null,
29
+            'message', null, null);
30
+        this.connection.addHandler(this.onMute.bind(this),
31
+            'http://jitsi.org/jitmeet/audio', 'iq', 'set',null,null);
32
+    }
91 33
 
92
-        setJingleSession: function (from, session) {
93
-            var room = this.rooms[Strophe.getBareJidFromJid(from)];
94
-            if(!room)
95
-                return;
34
+    createRoom (jid, password, options, settings) {
35
+        const roomJid = Strophe.getBareJidFromJid(jid);
36
+        if (this.rooms[roomJid]) {
37
+            const errmsg = "You are already in the room!";
38
+            logger.error(errmsg);
39
+            throw new Error(errmsg);
40
+        }
41
+        this.rooms[roomJid] = new ChatRoom(this.connection, jid,
42
+            password, this.xmpp, options, settings);
43
+        return this.rooms[roomJid];
44
+    }
96 45
 
97
-            room.setJingleSession(session);
98
-        },
46
+    doLeave (jid) {
47
+        delete this.rooms[jid];
48
+    }
99 49
 
100
-        onMute: function(iq) {
101
-            var from = iq.getAttribute('from');
102
-            var room = this.rooms[Strophe.getBareJidFromJid(from)];
103
-            if(!room)
104
-                return;
50
+    onPresence (pres) {
51
+        const from = pres.getAttribute('from');
105 52
 
106
-            room.onMute(iq);
53
+        // What is this for? A workaround for something?
54
+        if (pres.getAttribute('type')) {
107 55
             return true;
108 56
         }
109
-    });
110
-};
57
+
58
+        const room = this.rooms[Strophe.getBareJidFromJid(from)];
59
+        if(!room)
60
+            return;
61
+
62
+        // Parse status.
63
+        if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]' +
64
+            '>status[code="201"]').length) {
65
+            room.createNonAnonymousRoom();
66
+        }
67
+
68
+        room.onPresence(pres);
69
+
70
+        return true;
71
+    }
72
+
73
+    onPresenceUnavailable (pres) {
74
+        const from = pres.getAttribute('from');
75
+        const room = this.rooms[Strophe.getBareJidFromJid(from)];
76
+        if(!room)
77
+            return;
78
+
79
+        room.onPresenceUnavailable(pres, from);
80
+        return true;
81
+    }
82
+
83
+    onPresenceError (pres) {
84
+        const from = pres.getAttribute('from');
85
+        const room = this.rooms[Strophe.getBareJidFromJid(from)];
86
+        if(!room)
87
+            return;
88
+
89
+        room.onPresenceError(pres, from);
90
+        return true;
91
+    }
92
+
93
+    onMessage (msg) {
94
+        // FIXME: this is a hack. but jingle on muc makes nickchanges hard
95
+        const from = msg.getAttribute('from');
96
+        const room = this.rooms[Strophe.getBareJidFromJid(from)];
97
+        if(!room)
98
+            return;
99
+
100
+        room.onMessage(msg, from);
101
+        return true;
102
+    }
103
+
104
+    setJingleSession (from, session) {
105
+        const room = this.rooms[Strophe.getBareJidFromJid(from)];
106
+        if(!room)
107
+            return;
108
+
109
+        room.setJingleSession(session);
110
+    }
111
+
112
+    onMute(iq) {
113
+        const from = iq.getAttribute('from');
114
+        const room = this.rooms[Strophe.getBareJidFromJid(from)];
115
+        if(!room)
116
+            return;
117
+
118
+        room.onMute(iq);
119
+        return true;
120
+    }
121
+}
122
+
123
+export default function(XMPP) {
124
+    Strophe.addConnectionPlugin('emuc', new MucConnectionPlugin(XMPP));
125
+}

+ 243
- 222
modules/xmpp/strophe.jingle.js View File

@@ -1,243 +1,264 @@
1
-/* jshint -W117 */
1
+/* global $, $iq, Strophe */
2 2
 
3
+import { getLogger } from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
5
+import JingleSession from "./JingleSessionPC";
6
+import XMPPEvents from "../../service/xmpp/XMPPEvents";
7
+import GlobalOnErrorHandler from "../util/GlobalOnErrorHandler";
8
+import Statistics from "../statistics/statistics";
9
+import ConnectionPlugin from "./ConnectionPlugin";
3 10
 
4
-var logger = require("jitsi-meet-logger").getLogger(__filename);
5
-var JingleSession = require("./JingleSessionPC");
6
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
7
-var RTCBrowserType = require("../RTC/RTCBrowserType");
8
-var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
9
-
10
-
11
-module.exports = function(XMPP, eventEmitter) {
12
-    Strophe.addConnectionPlugin('jingle', {
13
-        connection: null,
14
-        sessions: {},
15
-        ice_config: {iceServers: []},
16
-        media_constraints: {
11
+class JingleConnectionPlugin extends ConnectionPlugin {
12
+    constructor(xmpp, eventEmitter) {
13
+        super();
14
+        this.xmpp = xmpp;
15
+        this.eventEmitter = eventEmitter;
16
+        this.sessions = {};
17
+        this.ice_config = {iceServers: []};
18
+        this.media_constraints = {
17 19
             mandatory: {
18 20
                 'OfferToReceiveAudio': true,
19 21
                 'OfferToReceiveVideo': true
20 22
             }
21 23
             // MozDontOfferDataChannel: true when this is firefox
22
-        },
23
-        init: function (conn) {
24
-            this.connection = conn;
25
-            this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
26
-        },
27
-        onJingle: function (iq) {
28
-            var sid = $(iq).find('jingle').attr('sid');
29
-            var action = $(iq).find('jingle').attr('action');
30
-            var fromJid = iq.getAttribute('from');
31
-            // send ack first
32
-            var ack = $iq({type: 'result',
33
-                to: fromJid,
34
-                id: iq.getAttribute('id')
35
-            });
36
-            logger.log('on jingle ' + action + ' from ' + fromJid, iq);
37
-            var sess = this.sessions[sid];
38
-            if ('session-initiate' != action) {
39
-                if (!sess) {
40
-                    ack.attrs({ type: 'error' });
41
-                    ack.c('error', {type: 'cancel'})
42
-                        .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
43
-                        .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
44
-                    logger.warn('invalid session id', iq);
45
-                    this.connection.send(ack);
46
-                    return true;
47
-                }
48
-                // local jid is not checked
49
-                if (fromJid != sess.peerjid) {
50
-                    logger.warn(
51
-                        'jid mismatch for session id', sid, sess.peerjid, iq);
52
-                    ack.attrs({ type: 'error' });
53
-                    ack.c('error', {type: 'cancel'})
54
-                        .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
55
-                        .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
56
-                    this.connection.send(ack);
57
-                    return true;
58
-                }
59
-            } else if (sess !== undefined) {
60
-                // existing session with same session id
61
-                // this might be out-of-order if the sess.peerjid is the same as from
24
+        };
25
+    }
26
+
27
+    init (connection) {
28
+        super.init(connection);
29
+        this.connection.addHandler(this.onJingle.bind(this),
30
+            'urn:xmpp:jingle:1', 'iq', 'set', null, null);
31
+    }
32
+
33
+    onJingle (iq) {
34
+        const sid = $(iq).find('jingle').attr('sid');
35
+        const action = $(iq).find('jingle').attr('action');
36
+        const fromJid = iq.getAttribute('from');
37
+        // send ack first
38
+        const ack = $iq({type: 'result',
39
+            to: fromJid,
40
+            id: iq.getAttribute('id')
41
+        });
42
+        logger.log('on jingle ' + action + ' from ' + fromJid, iq);
43
+        let sess = this.sessions[sid];
44
+        if ('session-initiate' != action) {
45
+            if (!sess) {
62 46
                 ack.attrs({ type: 'error' });
63 47
                 ack.c('error', {type: 'cancel'})
64
-                    .c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
65
-                logger.warn('duplicate session id', sid, iq);
48
+                    .c('item-not-found', {
49
+                        xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
50
+                    .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
51
+                logger.warn('invalid session id', iq);
66 52
                 this.connection.send(ack);
67 53
                 return true;
68 54
             }
69
-            // see http://xmpp.org/extensions/xep-0166.html#concepts-session
70
-            switch (action) {
71
-                case 'session-initiate':
72
-                    var now = window.performance.now();
73
-                    logger.log("(TIME) received session-initiate:\t", now);
74
-                    var startMuted = $(iq).find('jingle>startmuted');
75
-                    if (startMuted && startMuted.length > 0) {
76
-                        var audioMuted = startMuted.attr("audio");
77
-                        var videoMuted = startMuted.attr("video");
78
-                        eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
79
-                                audioMuted === "true", videoMuted === "true");
80
-                    }
81
-                    sess = new JingleSession(
82
-                            $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
83
-                            fromJid,
84
-                            this.connection,
85
-                            this.media_constraints,
86
-                            this.ice_config, XMPP);
87
-
88
-                    this.sessions[sess.sid] = sess;
89
-
90
-                    var jingleOffer = $(iq).find('>jingle');
91
-                    // FIXME there's no nice way with event to get the reason
92
-                    // why the call was rejected
93
-                    eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess, jingleOffer, now);
94
-                    if (!sess.active())
95
-                    {
96
-                        // Call not accepted
97
-                        ack.attrs({ type: 'error' });
98
-                        ack.c('error', {type: 'cancel'})
99
-                           .c('bad-request',
100
-                            { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
101
-                            .up();
102
-                        this.terminate(sess.sid);
103
-                    }
104
-                    break;
105
-                case 'session-terminate':
106
-                    logger.log('terminating...', sess.sid);
107
-                    var reasonCondition = null;
108
-                    var reasonText = null;
109
-                    if ($(iq).find('>jingle>reason').length) {
110
-                        reasonCondition
111
-                            = $(iq).find('>jingle>reason>:first')[0].tagName;
112
-                        reasonText = $(iq).find('>jingle>reason>text').text();
113
-                    }
114
-                    this.terminate(sess.sid, reasonCondition, reasonText);
115
-                    break;
116
-                case 'transport-replace':
117
-                    logger.info("(TIME) Start transport replace",
118
-                                window.performance.now());
119
-                    sess.replaceTransport($(iq).find('>jingle'),
120
-                        function () {
121
-                            logger.info(
122
-                                "(TIME) Transport replace success!",
123
-                                window.performance.now());
124
-                        },
125
-                        function(error) {
126
-                            GlobalOnErrorHandler.callErrorHandler(error);
127
-                            logger.error('Transport replace failed', error);
128
-                            sess.sendTransportReject();
129
-                        });
130
-                    break;
131
-                case 'addsource': // FIXME: proprietary, un-jingleish
132
-                case 'source-add': // FIXME: proprietary
133
-                    sess.addSource($(iq).find('>jingle>content'));
134
-                    break;
135
-                case 'removesource': // FIXME: proprietary, un-jingleish
136
-                case 'source-remove': // FIXME: proprietary
137
-                    sess.removeSource($(iq).find('>jingle>content'));
138
-                    break;
139
-                default:
140
-                    logger.warn('jingle action not implemented', action);
141
-                    ack.attrs({ type: 'error' });
142
-                    ack.c('error', {type: 'cancel'})
143
-                        .c('bad-request',
144
-                            { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
145
-                        .up();
146
-                    break;
55
+            // local jid is not checked
56
+            if (fromJid != sess.peerjid) {
57
+                logger.warn(
58
+                    'jid mismatch for session id', sid, sess.peerjid, iq);
59
+                ack.attrs({ type: 'error' });
60
+                ack.c('error', {type: 'cancel'})
61
+                    .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
62
+                    .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
63
+                this.connection.send(ack);
64
+                return true;
147 65
             }
66
+        } else if (sess !== undefined) {
67
+            // existing session with same session id
68
+            // this might be out-of-order if the sess.peerjid is the same as from
69
+            ack.attrs({ type: 'error' });
70
+            ack.c('error', {type: 'cancel'})
71
+                .c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
72
+            logger.warn('duplicate session id', sid, iq);
148 73
             this.connection.send(ack);
149 74
             return true;
150
-        },
151
-        terminate: function (sid, reasonCondition, reasonText) {
152
-            if (this.sessions.hasOwnProperty(sid)) {
153
-                if (this.sessions[sid].state != 'ended') {
154
-                    this.sessions[sid].onTerminated(reasonCondition, reasonText);
75
+        }
76
+        const now = window.performance.now();
77
+        // see http://xmpp.org/extensions/xep-0166.html#concepts-session
78
+        switch (action) {
79
+            case 'session-initiate': {
80
+                logger.log("(TIME) received session-initiate:\t", now);
81
+                const startMuted = $(iq).find('jingle>startmuted');
82
+                if (startMuted && startMuted.length > 0) {
83
+                    const audioMuted = startMuted.attr("audio");
84
+                    const videoMuted = startMuted.attr("video");
85
+                    this.eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
86
+                            audioMuted === "true", videoMuted === "true");
155 87
                 }
156
-                delete this.sessions[sid];
88
+                sess = new JingleSession(
89
+                        $(iq).attr('to'), $(iq).find('jingle').attr('sid'),
90
+                        fromJid,
91
+                        this.connection,
92
+                        this.media_constraints,
93
+                        this.ice_config, this.xmpp);
94
+
95
+                this.sessions[sess.sid] = sess;
96
+
97
+                this.eventEmitter.emit(XMPPEvents.CALL_INCOMING,
98
+                    sess, $(iq).find('>jingle'), now);
99
+                Statistics.analytics.sendEvent(
100
+                    'xmpp.session-initiate', {value: now});
101
+                break;
157 102
             }
158
-        },
159
-        getStunAndTurnCredentials: function () {
160
-            // get stun and turn configuration from server via xep-0215
161
-            // uses time-limited credentials as described in
162
-            // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
163
-            //
164
-            // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
165
-            // for a prosody module which implements this
166
-            //
167
-            // currently, this doesn't work with updateIce and therefore credentials with a long
168
-            // validity have to be fetched before creating the peerconnection
169
-            // TODO: implement refresh via updateIce as described in
170
-            //      https://code.google.com/p/webrtc/issues/detail?id=1650
171
-            var self = this;
172
-            this.connection.sendIQ(
173
-                $iq({type: 'get', to: this.connection.domain})
174
-                    .c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
175
-                function (res) {
176
-                    var iceservers = [];
177
-                    $(res).find('>services>service').each(function (idx, el) {
178
-                        el = $(el);
179
-                        var dict = {};
180
-                        var type = el.attr('type');
181
-                        switch (type) {
182
-                            case 'stun':
183
-                                dict.url = 'stun:' + el.attr('host');
184
-                                if (el.attr('port')) {
185
-                                    dict.url += ':' + el.attr('port');
186
-                                }
187
-                                iceservers.push(dict);
188
-                                break;
189
-                            case 'turn':
190
-                            case 'turns':
191
-                                dict.url = type + ':';
192
-                                if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
193
-                                    if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
194
-                                        dict.url += el.attr('username') + '@';
195
-                                    } else {
196
-                                        dict.username = el.attr('username'); // only works in M28
197
-                                    }
198
-                                }
199
-                                dict.url += el.attr('host');
200
-                                if (el.attr('port') && el.attr('port') != '3478') {
201
-                                    dict.url += ':' + el.attr('port');
202
-                                }
203
-                                if (el.attr('transport') && el.attr('transport') != 'udp') {
204
-                                    dict.url += '?transport=' + el.attr('transport');
205
-                                }
206
-                                if (el.attr('password')) {
207
-                                    dict.credential = el.attr('password');
208
-                                }
209
-                                iceservers.push(dict);
210
-                                break;
211
-                        }
212
-                    });
213
-                    self.ice_config.iceServers = iceservers;
214
-                },
215
-                function (err) {
216
-                    logger.warn('getting turn credentials failed', err);
217
-                    logger.warn('is mod_turncredentials or similar installed?');
103
+            case 'session-terminate': {
104
+                logger.log('terminating...', sess.sid);
105
+                let reasonCondition = null;
106
+                let reasonText = null;
107
+                if ($(iq).find('>jingle>reason').length) {
108
+                    reasonCondition
109
+                        = $(iq).find('>jingle>reason>:first')[0].tagName;
110
+                    reasonText = $(iq).find('>jingle>reason>text').text();
218 111
                 }
219
-            );
220
-            // implement push?
221
-        },
112
+                this.terminate(sess.sid, reasonCondition, reasonText);
113
+                this.eventEmitter.emit(XMPPEvents.CALL_ENDED,
114
+                    sess, reasonCondition, reasonText);
115
+                break;
116
+            }
117
+            case 'transport-replace':
118
+                logger.info("(TIME) Start transport replace", now);
119
+                Statistics.analytics.sendEvent(
120
+                    'xmpp.transport-replace.start', {value: now});
222 121
 
223
-        /**
224
-         * Returns the data saved in 'updateLog' in a format to be logged.
225
-         */
226
-        getLog: function () {
227
-            var data = {};
228
-            var self = this;
229
-            Object.keys(this.sessions).forEach(function (sid) {
230
-                var session = self.sessions[sid];
231
-                if (session.peerconnection && session.peerconnection.updateLog) {
232
-                    // FIXME: should probably be a .dump call
233
-                    data["jingle_" + session.sid] = {
234
-                        updateLog: session.peerconnection.updateLog,
235
-                        stats: session.peerconnection.stats,
236
-                        url: window.location.href
237
-                    };
238
-                }
239
-            });
240
-            return data;
122
+                sess.replaceTransport($(iq).find('>jingle'), () => {
123
+                    const successTime = window.performance.now();
124
+                    logger.info(
125
+                        "(TIME) Transport replace success!", successTime);
126
+                    Statistics.analytics.sendEvent(
127
+                        'xmpp.transport-replace.success',
128
+                        {value: successTime});
129
+                }, error => {
130
+                    GlobalOnErrorHandler.callErrorHandler(error);
131
+                    logger.error('Transport replace failed', error);
132
+                    sess.sendTransportReject();
133
+                });
134
+                break;
135
+            case 'addsource': // FIXME: proprietary, un-jingleish
136
+            case 'source-add': // FIXME: proprietary
137
+                sess.addSource($(iq).find('>jingle>content'));
138
+                break;
139
+            case 'removesource': // FIXME: proprietary, un-jingleish
140
+            case 'source-remove': // FIXME: proprietary
141
+                sess.removeSource($(iq).find('>jingle>content'));
142
+                break;
143
+            default:
144
+                logger.warn('jingle action not implemented', action);
145
+                ack.attrs({ type: 'error' });
146
+                ack.c('error', {type: 'cancel'})
147
+                    .c('bad-request',
148
+                        { xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' })
149
+                    .up();
150
+                break;
151
+        }
152
+        this.connection.send(ack);
153
+        return true;
154
+    }
155
+
156
+    terminate (sid, reasonCondition, reasonText) {
157
+        if (this.sessions.hasOwnProperty(sid)) {
158
+            if (this.sessions[sid].state != 'ended') {
159
+                this.sessions[sid].onTerminated(reasonCondition, reasonText);
160
+            }
161
+            delete this.sessions[sid];
241 162
         }
242
-    });
163
+    }
164
+
165
+    getStunAndTurnCredentials () {
166
+        // get stun and turn configuration from server via xep-0215
167
+        // uses time-limited credentials as described in
168
+        // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
169
+        //
170
+        // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
171
+        // for a prosody module which implements this
172
+        //
173
+        // currently, this doesn't work with updateIce and therefore credentials with a long
174
+        // validity have to be fetched before creating the peerconnection
175
+        // TODO: implement refresh via updateIce as described in
176
+        //      https://code.google.com/p/webrtc/issues/detail?id=1650
177
+        this.connection.sendIQ(
178
+            $iq({type: 'get', to: this.connection.domain})
179
+                .c('services', {xmlns: 'urn:xmpp:extdisco:1'})
180
+                .c('service', {host: 'turn.' + this.connection.domain}),
181
+            res => {
182
+                let iceservers = [];
183
+                $(res).find('>services>service').each((idx, el) => {
184
+                    el = $(el);
185
+                    let dict = {};
186
+                    const type = el.attr('type');
187
+                    switch (type) {
188
+                        case 'stun':
189
+                            dict.url = 'stun:' + el.attr('host');
190
+                            if (el.attr('port')) {
191
+                                dict.url += ':' + el.attr('port');
192
+                            }
193
+                            iceservers.push(dict);
194
+                            break;
195
+                        case 'turn':
196
+                        case 'turns': {
197
+                            dict.url = type + ':';
198
+                            const username = el.attr('username');
199
+                            // https://code.google.com/p/webrtc/issues/detail?id=1508
200
+                            if (username) {
201
+                                if (navigator.userAgent.match(
202
+                                    /Chrom(e|ium)\/([0-9]+)\./)
203
+                                    && parseInt(
204
+                                        navigator.userAgent.match(
205
+                                            /Chrom(e|ium)\/([0-9]+)\./)[2],
206
+                                            10) < 28) {
207
+                                    dict.url += username + '@';
208
+                                } else {
209
+                                    // only works in M28
210
+                                    dict.username = username;
211
+                                }
212
+                            }
213
+                            dict.url += el.attr('host');
214
+                            const port = el.attr('port');
215
+                            if (port && port != '3478') {
216
+                                dict.url += ':' + el.attr('port');
217
+                            }
218
+                            const transport = el.attr('transport');
219
+                            if (transport && transport != 'udp') {
220
+                                dict.url += '?transport=' + transport;
221
+                            }
222
+
223
+                            dict.credential = el.attr('password')
224
+                                || dict.credential;
225
+                            iceservers.push(dict);
226
+                            break;
227
+                        }
228
+                    }
229
+                });
230
+                this.ice_config.iceServers = iceservers;
231
+            }, err => {
232
+                logger.warn('getting turn credentials failed', err);
233
+                logger.warn('is mod_turncredentials or similar installed?');
234
+            });
235
+        // implement push?
236
+    }
237
+
238
+    /**
239
+     * Returns the data saved in 'updateLog' in a format to be logged.
240
+     */
241
+    getLog () {
242
+        const data = {};
243
+        Object.keys(this.sessions).forEach(sid => {
244
+            const session = this.sessions[sid];
245
+            const pc = session.peerconnection;
246
+            if (pc && pc.updateLog) {
247
+                // FIXME: should probably be a .dump call
248
+                data["jingle_" + sid] = {
249
+                    updateLog: pc.updateLog,
250
+                    stats: pc.stats,
251
+                    url: window.location.href
252
+                };
253
+            }
254
+        });
255
+        return data;
256
+    }
257
+}
258
+
259
+
260
+
261
+module.exports = function(XMPP, eventEmitter) {
262
+    Strophe.addConnectionPlugin('jingle',
263
+        new JingleConnectionPlugin(XMPP, eventEmitter));
243 264
 };

+ 28
- 18
modules/xmpp/strophe.logger.js View File

@@ -1,20 +1,30 @@
1 1
 /* global Strophe */
2
-module.exports = function () {
2
+import ConnectionPlugin from "./ConnectionPlugin";
3 3
 
4
-    Strophe.addConnectionPlugin('logger', {
5
-        // logs raw stanzas and makes them available for download as JSON
6
-        connection: null,
7
-        log: [],
8
-        init: function (conn) {
9
-            this.connection = conn;
10
-            this.connection.rawInput = this.log_incoming.bind(this);
11
-            this.connection.rawOutput = this.log_outgoing.bind(this);
12
-        },
13
-        log_incoming: function (stanza) {
14
-            this.log.push([new Date().getTime(), 'incoming', stanza]);
15
-        },
16
-        log_outgoing: function (stanza) {
17
-            this.log.push([new Date().getTime(), 'outgoing', stanza]);
18
-        }
19
-    });
20
-};
4
+/**
5
+ *  Logs raw stanzas and makes them available for download as JSON
6
+ */
7
+class StropheLogger extends ConnectionPlugin {
8
+    constructor() {
9
+        super();
10
+        this.log = [];
11
+    }
12
+
13
+    init (connection) {
14
+        super.init(connection);
15
+        this.connection.rawInput = this.log_incoming.bind(this);
16
+        this.connection.rawOutput = this.log_outgoing.bind(this);
17
+    }
18
+
19
+    log_incoming (stanza) {
20
+        this.log.push([new Date().getTime(), 'incoming', stanza]);
21
+    }
22
+
23
+    log_outgoing (stanza) {
24
+        this.log.push([new Date().getTime(), 'outgoing', stanza]);
25
+    }
26
+}
27
+
28
+export default function () {
29
+    Strophe.addConnectionPlugin('logger', new StropheLogger());
30
+}

+ 117
- 123
modules/xmpp/strophe.ping.js View File

@@ -1,149 +1,143 @@
1 1
 /* global $, $iq, Strophe */
2 2
 
3
-var logger = require("jitsi-meet-logger").getLogger(__filename);
4
-var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
5
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
3
+import { getLogger } from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
5
+import ConnectionPlugin from "./ConnectionPlugin";
6
+import GlobalOnErrorHandler from "../util/GlobalOnErrorHandler";
6 7
 
7 8
 /**
8 9
  * Ping every 10 sec
9 10
  */
10
-var PING_INTERVAL = 10000;
11
+const PING_INTERVAL = 10000;
11 12
 
12 13
 /**
13 14
  * Ping timeout error after 15 sec of waiting.
14 15
  */
15
-var PING_TIMEOUT = 15000;
16
+const PING_TIMEOUT = 15000;
16 17
 
17 18
 /**
18 19
  * Will close the connection after 3 consecutive ping errors.
19 20
  */
20
-var PING_THRESHOLD = 3;
21
+const PING_THRESHOLD = 3;
22
+
23
+
24
+
21 25
 
22 26
 /**
23 27
  * XEP-0199 ping plugin.
24 28
  *
25 29
  * Registers "urn:xmpp:ping" namespace under Strophe.NS.PING.
26 30
  */
27
-module.exports = function (XMPP, eventEmitter) {
28
-    Strophe.addConnectionPlugin('ping', {
29
-
30
-        connection: null,
31
-
32
-        failedPings: 0,
31
+class PingConnectionPlugin extends ConnectionPlugin {
32
+    constructor() {
33
+        super();
34
+        this.failedPings = 0;
35
+    }
33 36
 
34
-        /**
35
-         * Initializes the plugin. Method called by Strophe.
36
-         * @param connection Strophe connection instance.
37
-         */
38
-        init: function (connection) {
39
-            this.connection = connection;
40
-            Strophe.addNamespace('PING', "urn:xmpp:ping");
41
-        },
37
+    /**
38
+     * Initializes the plugin. Method called by Strophe.
39
+     * @param connection Strophe connection instance.
40
+     */
41
+    init (connection) {
42
+        super.init(connection);
43
+        Strophe.addNamespace('PING', "urn:xmpp:ping");
44
+    }
42 45
 
43
-        /**
44
-         * Sends "ping" to given <tt>jid</tt>
45
-         * @param jid the JID to which ping request will be sent.
46
-         * @param success callback called on success.
47
-         * @param error callback called on error.
48
-         * @param timeout ms how long are we going to wait for the response. On
49
-         *        timeout <tt>error<//t> callback is called with undefined error
50
-         *        argument.
51
-         */
52
-        ping: function (jid, success, error, timeout) {
53
-            var iq = $iq({type: 'get', to: jid});
54
-            iq.c('ping', {xmlns: Strophe.NS.PING});
55
-            this.connection.sendIQ(iq, success, error, timeout);
56
-        },
46
+    /**
47
+     * Sends "ping" to given <tt>jid</tt>
48
+     * @param jid the JID to which ping request will be sent.
49
+     * @param success callback called on success.
50
+     * @param error callback called on error.
51
+     * @param timeout ms how long are we going to wait for the response. On
52
+     *        timeout <tt>error<//t> callback is called with undefined error
53
+     *        argument.
54
+     */
55
+    ping (jid, success, error, timeout) {
56
+        const iq = $iq({type: 'get', to: jid});
57
+        iq.c('ping', {xmlns: Strophe.NS.PING});
58
+        this.connection.sendIQ(iq, success, error, timeout);
59
+    }
57 60
 
58
-        /**
59
-         * Checks if given <tt>jid</tt> has XEP-0199 ping support.
60
-         * @param jid the JID to be checked for ping support.
61
-         * @param callback function with boolean argument which will be
62
-         * <tt>true</tt> if XEP-0199 ping is supported by given <tt>jid</tt>
63
-         */
64
-        hasPingSupport: function (jid, callback) {
65
-            var disco = this.connection.disco;
66
-            // XXX The following disco.info was observed to throw a "TypeError:
67
-            // Cannot read property 'info' of undefined" during porting to React
68
-            // Native. Since disco is checked in multiple places (e.g.
69
-            // strophe.jingle.js, strophe.rayo.js), check it here as well.
70
-            if (disco) {
71
-                disco.info(
72
-                    jid,
73
-                    null,
74
-                    function (result) {
75
-                        var ping
76
-                            = $(result).find('>>feature[var="urn:xmpp:ping"]');
77
-                        callback(ping.length > 0);
78
-                    },
79
-                    function (error) {
80
-                        var errmsg = "Ping feature discovery error";
81
-                        GlobalOnErrorHandler.callErrorHandler(new Error(
82
-                            errmsg + ": " + error));
83
-                        logger.error(errmsg, error);
84
-                        callback(false);
85
-                    }
86
-                );
87
-            } else {
88
-              // FIXME Should callback be invoked here? Maybe with false as an
89
-              // argument?
90
-            }
91
-        },
92
-
93
-        /**
94
-         * Starts to send ping in given interval to specified remote JID.
95
-         * This plugin supports only one such task and <tt>stopInterval</tt>
96
-         * must be called before starting a new one.
97
-         * @param remoteJid remote JID to which ping requests will be sent to.
98
-         * @param interval task interval in ms.
99
-         */
100
-        startInterval: function (remoteJid, interval) {
101
-            if (this.intervalId) {
102
-                var errmsg = "Ping task scheduled already";
103
-                GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
104
-                logger.error(errmsg);
105
-                return;
106
-            }
107
-            if (!interval)
108
-                interval = PING_INTERVAL;
109
-            var self = this;
110
-            this.intervalId = window.setInterval(function () {
111
-                self.ping(remoteJid,
112
-                function (result) {
113
-                    // Ping OK
114
-                    self.failedPings = 0;
115
-                },
116
-                function (error) {
117
-                    self.failedPings += 1;
118
-                    var errmsg = "Ping " + (error ? "error" : "timeout");
119
-                    if (self.failedPings >= PING_THRESHOLD) {
120
-                        GlobalOnErrorHandler.callErrorHandler(
121
-                            new Error(errmsg));
122
-                        logger.error(errmsg, error);
123
-                        // FIXME it doesn't help to disconnect when 3rd PING
124
-                        // times out, it only stops Strophe from retrying.
125
-                        // Not really sure what's the right thing to do in that
126
-                        // situation, but just closing the connection makes no
127
-                        // sense.
128
-                        //self.connection.disconnect();
129
-                    } else {
130
-                        logger.warn(errmsg, error);
131
-                    }
132
-                }, PING_TIMEOUT);
133
-            }, interval);
134
-            logger.info("XMPP pings will be sent every " + interval + " ms");
135
-        },
61
+    /**
62
+     * Checks if given <tt>jid</tt> has XEP-0199 ping support.
63
+     * @param jid the JID to be checked for ping support.
64
+     * @param callback function with boolean argument which will be
65
+     * <tt>true</tt> if XEP-0199 ping is supported by given <tt>jid</tt>
66
+     */
67
+    hasPingSupport (jid, callback) {
68
+        const disco = this.connection.disco;
69
+        // XXX The following disco.info was observed to throw a "TypeError:
70
+        // Cannot read property 'info' of undefined" during porting to React
71
+        // Native. Since disco is checked in multiple places (e.g.
72
+        // strophe.jingle.js, strophe.rayo.js), check it here as well.
73
+        if (disco) {
74
+            disco.info(jid, null, (result)  => {
75
+                const ping
76
+                    = $(result).find('>>feature[var="urn:xmpp:ping"]');
77
+                callback(ping.length > 0);
78
+            }, (error) => {
79
+                const errmsg = "Ping feature discovery error";
80
+                GlobalOnErrorHandler.callErrorHandler(new Error(
81
+                    errmsg + ": " + error));
82
+                logger.error(errmsg, error);
83
+                callback(false);
84
+            });
85
+        } else {
86
+          // FIXME Should callback be invoked here? Maybe with false as an
87
+          // argument?
88
+        }
89
+    }
136 90
 
137
-        /**
138
-         * Stops current "ping"  interval task.
139
-         */
140
-        stopInterval: function () {
141
-            if (this.intervalId) {
142
-                window.clearInterval(this.intervalId);
143
-                this.intervalId = null;
91
+    /**
92
+     * Starts to send ping in given interval to specified remote JID.
93
+     * This plugin supports only one such task and <tt>stopInterval</tt>
94
+     * must be called before starting a new one.
95
+     * @param remoteJid remote JID to which ping requests will be sent to.
96
+     * @param interval task interval in ms.
97
+     */
98
+    startInterval (remoteJid, interval = PING_INTERVAL) {
99
+        if (this.intervalId) {
100
+            const errmsg = "Ping task scheduled already";
101
+            GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
102
+            logger.error(errmsg);
103
+            return;
104
+        }
105
+        this.intervalId = window.setInterval(() => {
106
+            this.ping(remoteJid, () => {
144 107
                 this.failedPings = 0;
145
-                logger.info("Ping interval cleared");
146
-            }
108
+            }, (error) => {
109
+                this.failedPings += 1;
110
+                const errmsg = "Ping " + (error ? "error" : "timeout");
111
+                if (this.failedPings >= PING_THRESHOLD) {
112
+                    GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
113
+                    logger.error(errmsg, error);
114
+                    // FIXME it doesn't help to disconnect when 3rd PING
115
+                    // times out, it only stops Strophe from retrying.
116
+                    // Not really sure what's the right thing to do in that
117
+                    // situation, but just closing the connection makes no
118
+                    // sense.
119
+                    //self.connection.disconnect();
120
+                } else {
121
+                    logger.warn(errmsg, error);
122
+                }
123
+            }, PING_TIMEOUT);
124
+        }, interval);
125
+        logger.info("XMPP pings will be sent every " + interval + " ms");
126
+    }
127
+
128
+    /**
129
+     * Stops current "ping"  interval task.
130
+     */
131
+    stopInterval () {
132
+        if (this.intervalId) {
133
+            window.clearInterval(this.intervalId);
134
+            this.intervalId = null;
135
+            this.failedPings = 0;
136
+            logger.info("Ping interval cleared");
147 137
         }
148
-    });
149
-};
138
+    }
139
+}
140
+
141
+export default function () {
142
+    Strophe.addConnectionPlugin('ping', new PingConnectionPlugin());
143
+}

+ 89
- 105
modules/xmpp/strophe.rayo.js View File

@@ -1,114 +1,98 @@
1
-/* jshint -W117 */
2
-var logger = require("jitsi-meet-logger").getLogger(__filename);
1
+/* global $, $iq, Strophe */
3 2
 
4
-module.exports = function() {
5
-    Strophe.addConnectionPlugin('rayo',
6
-        {
7
-            RAYO_XMLNS: 'urn:xmpp:rayo:1',
8
-            connection: null,
9
-            init: function (conn) {
10
-                this.connection = conn;
11
-                var disco = conn.disco;
12
-                if (disco) {
13
-                    disco.addFeature('urn:xmpp:rayo:client:1');
14
-                }
3
+import { getLogger } from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
5
+import ConnectionPlugin from "./ConnectionPlugin";
15 6
 
16
-                this.connection.addHandler(
17
-                    this.onRayo.bind(this), this.RAYO_XMLNS, 'iq', 'set',
18
-                    null, null);
19
-            },
20
-            onRayo: function (iq) {
21
-                logger.info("Rayo IQ", iq);
22
-            },
23
-            dial: function (to, from, roomName, roomPass, focusMucJid) {
24
-                var self = this;
25
-                return new Promise(function (resolve, reject) {
26
-                    if(!focusMucJid) {
27
-                        reject(new Error("Internal error!"));
28
-                        return;
29
-                    }
30
-                    var req = $iq(
31
-                        {
32
-                            type: 'set',
33
-                            to: focusMucJid
34
-                        }
35
-                    );
36
-                    req.c('dial',
37
-                        {
38
-                            xmlns: self.RAYO_XMLNS,
39
-                            to: to,
40
-                            from: from
41
-                        });
42
-                    req.c('header',
43
-                        {
44
-                            name: 'JvbRoomName',
45
-                            value: roomName
46
-                        }).up();
7
+const RAYO_XMLNS = 'urn:xmpp:rayo:1';
47 8
 
48
-                    if (roomPass !== null && roomPass.length) {
9
+class RayoConnectionPlugin extends ConnectionPlugin {
10
+    init (connection) {
11
+        super.init(connection);
12
+        const disco = this.connection.disco;
13
+        if (disco) {
14
+            disco.addFeature('urn:xmpp:rayo:client:1');
15
+        }
16
+
17
+        this.connection.addHandler(
18
+            this.onRayo.bind(this), RAYO_XMLNS, 'iq', 'set', null, null);
19
+    }
49 20
 
50
-                        req.c('header',
51
-                            {
52
-                                name: 'JvbRoomPassword',
53
-                                value: roomPass
54
-                            }).up();
55
-                    }
21
+    onRayo (iq) {
22
+        logger.info("Rayo IQ", iq);
23
+    }
56 24
 
57
-                    self.connection.sendIQ(
58
-                        req,
59
-                        function (result) {
60
-                            logger.info('Dial result ', result);
25
+    dial (to, from, roomName, roomPass, focusMucJid) {
26
+        return new Promise((resolve, reject) => {
27
+            if(!focusMucJid) {
28
+                reject(new Error("Internal error!"));
29
+                return;
30
+            }
31
+            const req = $iq({
32
+                type: 'set',
33
+                to: focusMucJid
34
+            });
35
+            req.c('dial', {
36
+                xmlns: RAYO_XMLNS,
37
+                to: to,
38
+                from: from
39
+            });
40
+            req.c('header', {
41
+                name: 'JvbRoomName',
42
+                value: roomName
43
+            }).up();
44
+
45
+            if (roomPass && roomPass.length) {
46
+                req.c('header', {
47
+                    name: 'JvbRoomPassword',
48
+                    value: roomPass
49
+                }).up();
50
+            }
61 51
 
62
-                            var resource = $(result).find('ref').attr('uri');
63
-                            self.call_resource =
64
-                                resource.substr('xmpp:'.length);
65
-                            logger.info(
66
-                                "Received call resource: " +
67
-                                self.call_resource);
68
-                            resolve();
69
-                        },
70
-                        function (error) {
71
-                            logger.info('Dial error ', error);
72
-                            reject(error);
73
-                        }
74
-                    );
75
-                });
76
-            },
77
-            hangup: function () {
78
-                var self = this;
79
-                return new Promise(function (resolve, reject) {
80
-                    if (!self.call_resource) {
81
-                        reject(new Error("No call in progress"));
82
-                        logger.warn("No call in progress");
83
-                        return;
84
-                    }
52
+            this.connection.sendIQ(req, (result) => {
53
+                logger.info('Dial result ', result);
85 54
 
86
-                    var req = $iq(
87
-                        {
88
-                            type: 'set',
89
-                            to: self.call_resource
90
-                        }
91
-                    );
92
-                    req.c('hangup',
93
-                        {
94
-                            xmlns: self.RAYO_XMLNS
95
-                        });
55
+                let resource = $(result).find('ref').attr('uri');
56
+                this.call_resource =
57
+                    resource.substr('xmpp:'.length);
58
+                logger.info("Received call resource: " + this.call_resource);
59
+                resolve();
60
+            }, (error) => {
61
+                logger.info('Dial error ', error);
62
+                reject(error);
63
+            });
64
+        });
65
+    }
96 66
 
97
-                    self.connection.sendIQ(
98
-                        req,
99
-                        function (result) {
100
-                            logger.info('Hangup result ', result);
101
-                            self.call_resource = null;
102
-                            resolve();
103
-                        },
104
-                        function (error) {
105
-                            logger.info('Hangup error ', error);
106
-                            self.call_resource = null;
107
-                            reject(new Error('Hangup error '));
108
-                        }
109
-                    );
110
-                });
67
+    hangup () {
68
+        return new Promise((resolve, reject) => {
69
+            if (!this.call_resource) {
70
+                reject(new Error("No call in progress"));
71
+                logger.warn("No call in progress");
72
+                return;
111 73
             }
112
-        }
113
-    );
114
-};
74
+
75
+            const req = $iq({
76
+                type: 'set',
77
+                to: this.call_resource
78
+            });
79
+            req.c('hangup', {
80
+                xmlns: RAYO_XMLNS
81
+            });
82
+
83
+            this.connection.sendIQ(req, (result) => {
84
+                logger.info('Hangup result ', result);
85
+                this.call_resource = null;
86
+                resolve();
87
+            }, (error) => {
88
+                logger.info('Hangup error ', error);
89
+                this.call_resource = null;
90
+                reject(new Error('Hangup error '));
91
+            });
92
+        });
93
+    }
94
+}
95
+
96
+export default function() {
97
+    Strophe.addConnectionPlugin('rayo', new RayoConnectionPlugin());
98
+}

+ 5
- 4
modules/xmpp/strophe.util.js View File

@@ -2,10 +2,11 @@
2 2
 /**
3 3
  * Strophe logger implementation. Logs from level WARN and above.
4 4
  */
5
-var logger = require("jitsi-meet-logger").getLogger(__filename);
6
-var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
5
+import {getLogger} from "jitsi-meet-logger";
6
+const logger = getLogger(__filename);
7
+import GlobalOnErrorHandler from "../util/GlobalOnErrorHandler";
7 8
 
8
-module.exports = function () {
9
+export default function () {
9 10
 
10 11
     Strophe.log = function (level, msg) {
11 12
         // Our global handler reports uncaught errors to the stats which may
@@ -55,4 +56,4 @@ module.exports = function () {
55 56
                 return "unknown";
56 57
         }
57 58
     };
58
-};
59
+}

+ 343
- 308
modules/xmpp/xmpp.js View File

@@ -1,68 +1,67 @@
1
-/* global $, APP, config, Strophe */
2
-
3
-var logger = require("jitsi-meet-logger").getLogger(__filename);
4
-var EventEmitter = require("events");
5
-var Pako = require("pako");
6
-var RandomUtil = require("../util/RandomUtil");
7
-var RTCEvents = require("../../service/RTC/RTCEvents");
8
-var XMPPEvents = require("../../service/xmpp/XMPPEvents");
9
-var JitsiConnectionErrors = require("../../JitsiConnectionErrors");
10
-var JitsiConnectionEvents = require("../../JitsiConnectionEvents");
11
-var RTC = require("../RTC/RTC");
12
-var RTCBrowserType = require("../RTC/RTCBrowserType");
13
-
14
-var authenticatedUser = false;
15
-
16
-function createConnection(bosh, token) {
17
-    bosh = bosh || '/http-bind';
18
-
1
+/* global $, $msg, Base64, Strophe */
2
+
3
+import { getLogger } from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
5
+import EventEmitter from "events";
6
+import Pako from "pako";
7
+import RandomUtil from "../util/RandomUtil";
8
+import * as JitsiConnectionErrors from "../../JitsiConnectionErrors";
9
+import * as JitsiConnectionEvents from "../../JitsiConnectionEvents";
10
+import RTCBrowserType from "../RTC/RTCBrowserType";
11
+import initEmuc from "./strophe.emuc";
12
+import initJingle from "./strophe.jingle";
13
+import initStropheUtil from "./strophe.util";
14
+import initPing from "./strophe.ping";
15
+import initRayo from "./strophe.rayo";
16
+import initStropheLogger from "./strophe.logger";
17
+
18
+function createConnection(token, bosh = '/http-bind') {
19 19
     // Append token as URL param
20 20
     if (token) {
21 21
         bosh += (bosh.indexOf('?') == -1 ? '?' : '&') + 'token=' + token;
22 22
     }
23 23
 
24 24
     return new Strophe.Connection(bosh);
25
-};
26
-
27
-//!!!!!!!!!! FIXME: ...
28
-function initStrophePlugins(XMPP) {
29
-    require("./strophe.emuc")(XMPP);
30
-    require("./strophe.jingle")(XMPP, XMPP.eventEmitter);
31
-//    require("./strophe.moderate")(XMPP, eventEmitter);
32
-    require("./strophe.util")();
33
-    require("./strophe.ping")(XMPP, XMPP.eventEmitter);
34
-    require("./strophe.rayo")();
35
-    require("./strophe.logger")();
36 25
 }
37 26
 
38
-function XMPP(options, token) {
39
-    this.eventEmitter = new EventEmitter();
40
-    this.connection = null;
41
-    this.disconnectInProgress = false;
42
-    this.connectionTimes = {};
43
-    this.forceMuted = false;
44
-    this.options = options;
45
-    initStrophePlugins(this);
46
-
47
-    this.connection = createConnection(options.bosh, token);
48
-
49
-    // Initialize features advertised in disco-info
50
-    this.initFeaturesList();
51
-
52
-    // Setup a disconnect on unload as a way to facilitate API consumers. It
53
-    // sounds like they would want that. A problem for them though may be if
54
-    // they wanted to utilize the connected connection in an unload handler of
55
-    // their own. However, it should be fairly easy for them to do that by
56
-    // registering their unload handler before us.
57
-    $(window).on('beforeunload unload', this.disconnect.bind(this));
58
-}
27
+export default class XMPP {
28
+    constructor(options, token) {
29
+        this.eventEmitter = new EventEmitter();
30
+        this.connection = null;
31
+        this.disconnectInProgress = false;
32
+        this.connectionTimes = {};
33
+        this.forceMuted = false;
34
+        this.options = options;
35
+        this.connectParams = {};
36
+        this.token = token;
37
+        this.authenticatedUser = false;
38
+        this._initStrophePlugins(this);
39
+
40
+        this.connection = createConnection(token, options.bosh);
41
+
42
+        if(!this.connection.disco || !this.connection.caps)
43
+            throw new Error(
44
+                "Missing strophe-plugins (disco and caps plugins are required)!");
45
+
46
+        // Initialize features advertised in disco-info
47
+        this.initFeaturesList();
48
+
49
+        // Setup a disconnect on unload as a way to facilitate API consumers. It
50
+        // sounds like they would want that. A problem for them though may be if
51
+        // they wanted to utilize the connected connection in an unload handler of
52
+        // their own. However, it should be fairly easy for them to do that by
53
+        // registering their unload handler before us.
54
+        $(window).on('beforeunload unload', this.disconnect.bind(this));
55
+    }
56
+
57
+    /**
58
+     * Initializes the list of feature advertised through the disco-info mechanism
59
+     */
60
+    initFeaturesList () {
61
+        const disco = this.connection.disco;
62
+        if (!disco)
63
+            return;
59 64
 
60
-/**
61
- * Initializes the list of feature advertised through the disco-info mechanism
62
- */
63
-XMPP.prototype.initFeaturesList = function () {
64
-    var disco = this.connection.disco;
65
-    if (disco) {
66 65
         // http://xmpp.org/extensions/xep-0167.html#support
67 66
         // http://xmpp.org/extensions/xep-0176.html#support
68 67
         disco.addFeature('urn:xmpp:jingle:1');
@@ -88,286 +87,322 @@ XMPP.prototype.initFeaturesList = function () {
88 87
         //disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
89 88
 
90 89
         // Enable Lipsync ?
91
-        if (this.options.enableLipSync && RTCBrowserType.isChrome()) {
90
+        if (RTCBrowserType.isChrome() && false !== this.options.enableLipSync) {
92 91
             logger.info("Lip-sync enabled !");
93
-            this.connection.disco.addFeature('http://jitsi.org/meet/lipsync');
92
+            disco.addFeature('http://jitsi.org/meet/lipsync');
94 93
         }
95 94
     }
96
-};
97
-
98
-XMPP.prototype.getConnection = function () { return this.connection; };
99
-
100
-/**
101
- * Receive connection status changes and handles them.
102
- * @password {string} the password passed in connect method
103
- * @status the connection status
104
- * @msg message
105
- */
106
-XMPP.prototype.connectionHandler = function (password, status, msg) {
107
-    var now = window.performance.now();
108
-    this.connectionTimes[Strophe.getStatusString(status).toLowerCase()] = now;
109
-    logger.log("(TIME) Strophe " + Strophe.getStatusString(status) +
110
-        (msg ? "[" + msg + "]" : "") + ":\t", now);
111
-    if (status === Strophe.Status.CONNECTED ||
112
-        status === Strophe.Status.ATTACHED) {
113
-        if (this.options.useStunTurn) {
114
-            this.connection.jingle.getStunAndTurnCredentials();
115
-        }
116 95
 
117
-        logger.info("My Jabber ID: " + this.connection.jid);
118
-
119
-        // Schedule ping ?
120
-        var pingJid = this.connection.domain;
121
-        this.connection.ping.hasPingSupport(
122
-            pingJid,
123
-            function (hasPing) {
124
-                if (hasPing)
125
-                    this.connection.ping.startInterval(pingJid);
126
-                else
127
-                    logger.warn("Ping NOT supported by " + pingJid);
128
-            }.bind(this));
129
-
130
-        if (password)
131
-            authenticatedUser = true;
132
-        if (this.connection && this.connection.connected &&
133
-            Strophe.getResourceFromJid(this.connection.jid)) {
134
-            // .connected is true while connecting?
135
-//                this.connection.send($pres());
136
-            this.eventEmitter.emit(
137
-                    JitsiConnectionEvents.CONNECTION_ESTABLISHED,
138
-                    Strophe.getResourceFromJid(this.connection.jid));
139
-        }
140
-    } else if (status === Strophe.Status.CONNFAIL) {
141
-        if (msg === 'x-strophe-bad-non-anon-jid') {
142
-            this.anonymousConnectionFailed = true;
143
-        } else {
144
-            this.connectionFailed = true;
145
-        }
146
-        this.lastErrorMsg = msg;
147
-    } else if (status === Strophe.Status.DISCONNECTED) {
148
-        // Stop ping interval
149
-        this.connection.ping.stopInterval();
150
-        this.disconnectInProgress = false;
151
-        if (this.anonymousConnectionFailed) {
152
-            // prompt user for username and password
96
+    getConnection () { return this.connection; }
97
+
98
+    /**
99
+     * Receive connection status changes and handles them.
100
+     * @password {string} the password passed in connect method
101
+     * @status the connection status
102
+     * @msg message
103
+     */
104
+    connectionHandler (password, status, msg) {
105
+        const now = window.performance.now();
106
+        const statusStr = Strophe.getStatusString(status).toLowerCase();
107
+        this.connectionTimes[statusStr] = now;
108
+        logger.log("(TIME) Strophe " + statusStr +
109
+            (msg ? "[" + msg + "]" : "") + ":\t", now);
110
+        if (status === Strophe.Status.CONNECTED ||
111
+            status === Strophe.Status.ATTACHED) {
112
+            if (this.options.useStunTurn) {
113
+                this.connection.jingle.getStunAndTurnCredentials();
114
+            }
115
+
116
+            logger.info("My Jabber ID: " + this.connection.jid);
117
+
118
+            // Schedule ping ?
119
+            var pingJid = this.connection.domain;
120
+            this.connection.ping.hasPingSupport(
121
+                pingJid,
122
+                function (hasPing) {
123
+                    if (hasPing)
124
+                        this.connection.ping.startInterval(pingJid);
125
+                    else
126
+                        logger.warn("Ping NOT supported by " + pingJid);
127
+                }.bind(this));
128
+
129
+            if (password)
130
+                this.authenticatedUser = true;
131
+            if (this.connection && this.connection.connected &&
132
+                Strophe.getResourceFromJid(this.connection.jid)) {
133
+                // .connected is true while connecting?
134
+    //                this.connection.send($pres());
135
+                this.eventEmitter.emit(
136
+                        JitsiConnectionEvents.CONNECTION_ESTABLISHED,
137
+                        Strophe.getResourceFromJid(this.connection.jid));
138
+            }
139
+        } else if (status === Strophe.Status.CONNFAIL) {
140
+            if (msg === 'x-strophe-bad-non-anon-jid') {
141
+                this.anonymousConnectionFailed = true;
142
+            } else {
143
+                this.connectionFailed = true;
144
+            }
145
+            this.lastErrorMsg = msg;
146
+        } else if (status === Strophe.Status.DISCONNECTED) {
147
+            // Stop ping interval
148
+            this.connection.ping.stopInterval();
149
+            const wasIntentionalDisconnect = this.disconnectInProgress;
150
+            const errMsg = msg ? msg : this.lastErrorMsg;
151
+            this.disconnectInProgress = false;
152
+            if (this.anonymousConnectionFailed) {
153
+                // prompt user for username and password
154
+                this.eventEmitter.emit(
155
+                    JitsiConnectionEvents.CONNECTION_FAILED,
156
+                    JitsiConnectionErrors.PASSWORD_REQUIRED);
157
+            } else if(this.connectionFailed) {
158
+                this.eventEmitter.emit(
159
+                    JitsiConnectionEvents.CONNECTION_FAILED,
160
+                    JitsiConnectionErrors.OTHER_ERROR, errMsg);
161
+            } else if (!wasIntentionalDisconnect) {
162
+                // XXX if Strophe drops the connection while not being asked to,
163
+                // it means that most likely some serious error has occurred.
164
+                // One currently known case is when a BOSH request fails for
165
+                // more than 4 times. The connection is dropped without
166
+                // supplying a reason(error message/event) through the API.
167
+                logger.error("XMPP connection dropped!");
168
+                this.eventEmitter.emit(
169
+                    JitsiConnectionEvents.CONNECTION_FAILED,
170
+                    JitsiConnectionErrors.OTHER_ERROR,
171
+                    errMsg ? errMsg : 'connection-dropped-error');
172
+            } else {
173
+                this.eventEmitter.emit(
174
+                    JitsiConnectionEvents.CONNECTION_DISCONNECTED, errMsg);
175
+            }
176
+        } else if (status === Strophe.Status.AUTHFAIL) {
177
+            // wrong password or username, prompt user
153 178
             this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
154 179
                 JitsiConnectionErrors.PASSWORD_REQUIRED);
155
-        } else if(this.connectionFailed) {
156
-            this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
157
-                JitsiConnectionErrors.OTHER_ERROR,
158
-                msg ? msg : this.lastErrorMsg);
159
-        } else {
160
-            this.eventEmitter.emit(
161
-                    JitsiConnectionEvents.CONNECTION_DISCONNECTED,
162
-                    msg ? msg : this.lastErrorMsg);
180
+
163 181
         }
164
-    } else if (status === Strophe.Status.AUTHFAIL) {
165
-        // wrong password or username, prompt user
166
-        this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
167
-            JitsiConnectionErrors.PASSWORD_REQUIRED);
182
+    }
168 183
 
184
+    _connect (jid, password) {
185
+        // connection.connect() starts the connection process.
186
+        //
187
+        // As the connection process proceeds, the user supplied callback will
188
+        // be triggered multiple times with status updates. The callback should
189
+        // take two arguments - the status code and the error condition.
190
+        //
191
+        // The status code will be one of the values in the Strophe.Status
192
+        // constants. The error condition will be one of the conditions defined
193
+        // in RFC 3920 or the condition ‘strophe-parsererror’.
194
+        //
195
+        // The Parameters wait, hold and route are optional and only relevant
196
+        // for BOSH connections. Please see XEP 124 for a more detailed
197
+        // explanation of the optional parameters.
198
+        //
199
+        // Connection status constants for use by the connection handler
200
+        // callback.
201
+        //
202
+        //  Status.ERROR - An error has occurred (websockets specific)
203
+        //  Status.CONNECTING - The connection is currently being made
204
+        //  Status.CONNFAIL - The connection attempt failed
205
+        //  Status.AUTHENTICATING - The connection is authenticating
206
+        //  Status.AUTHFAIL - The authentication attempt failed
207
+        //  Status.CONNECTED - The connection has succeeded
208
+        //  Status.DISCONNECTED - The connection has been terminated
209
+        //  Status.DISCONNECTING - The connection is currently being terminated
210
+        //  Status.ATTACHED - The connection has been attached
211
+
212
+        this.anonymousConnectionFailed = false;
213
+        this.connectionFailed = false;
214
+        this.lastErrorMsg = undefined;
215
+        this.connection.connect(jid, password,
216
+            this.connectionHandler.bind(this, password));
169 217
     }
170
-}
171 218
 
172
-XMPP.prototype._connect = function (jid, password) {
173
-    // connection.connect() starts the connection process.
174
-    //
175
-    // As the connection process proceeds, the user supplied callback will
176
-    // be triggered multiple times with status updates. The callback should
177
-    // take two arguments - the status code and the error condition.
178
-    //
179
-    // The status code will be one of the values in the Strophe.Status
180
-    // constants. The error condition will be one of the conditions defined
181
-    // in RFC 3920 or the condition ‘strophe-parsererror’.
182
-    //
183
-    // The Parameters wait, hold and route are optional and only relevant
184
-    // for BOSH connections. Please see XEP 124 for a more detailed
185
-    // explanation of the optional parameters.
186
-    //
187
-    // Connection status constants for use by the connection handler
188
-    // callback.
189
-    //
190
-    //  Status.ERROR - An error has occurred (websockets specific)
191
-    //  Status.CONNECTING - The connection is currently being made
192
-    //  Status.CONNFAIL - The connection attempt failed
193
-    //  Status.AUTHENTICATING - The connection is authenticating
194
-    //  Status.AUTHFAIL - The authentication attempt failed
195
-    //  Status.CONNECTED - The connection has succeeded
196
-    //  Status.DISCONNECTED - The connection has been terminated
197
-    //  Status.DISCONNECTING - The connection is currently being terminated
198
-    //  Status.ATTACHED - The connection has been attached
199
-
200
-    this.anonymousConnectionFailed = false;
201
-    this.connectionFailed = false;
202
-    this.lastErrorMsg = undefined;
203
-    this.connection.connect(jid, password,
204
-        this.connectionHandler.bind(this, password));
205
-}
219
+    /**
220
+     * Attach to existing connection. Can be used for optimizations. For example:
221
+     * if the connection is created on the server we can attach to it and start
222
+     * using it.
223
+     *
224
+     * @param options {object} connecting options - rid, sid, jid and password.
225
+     */
226
+    attach (options) {
227
+        const now = this.connectionTimes["attaching"] = window.performance.now();
228
+        logger.log("(TIME) Strophe Attaching\t:" + now);
229
+        this.connection.attach(options.jid, options.sid,
230
+            parseInt(options.rid,10)+1,
231
+            this.connectionHandler.bind(this, options.password));
232
+    }
206 233
 
207
-/**
208
- * Attach to existing connection. Can be used for optimizations. For example:
209
- * if the connection is created on the server we can attach to it and start
210
- * using it.
211
- *
212
- * @param options {object} connecting options - rid, sid, jid and password.
213
- */
214
- XMPP.prototype.attach = function (options) {
215
-    var now = this.connectionTimes["attaching"] = window.performance.now();
216
-    logger.log("(TIME) Strophe Attaching\t:" + now);
217
-    this.connection.attach(options.jid, options.sid, parseInt(options.rid,10)+1,
218
-        this.connectionHandler.bind(this, options.password));
219
-}
234
+    connect (jid, password) {
235
+        this.connectParams = {
236
+            jid: jid,
237
+            password: password
238
+        };
239
+        if (!jid) {
240
+            let configDomain
241
+                = this.options.hosts.anonymousdomain ||
242
+                    this.options.hosts.domain;
243
+            // Force authenticated domain if room is appended with '?login=true'
244
+            // or if we're joining with the token
245
+            if (this.options.hosts.anonymousdomain
246
+                    && (window.location.search.indexOf("login=true") !== -1
247
+                        || this.options.token)) {
248
+                configDomain = this.options.hosts.domain;
249
+            }
250
+            jid = configDomain || window.location.hostname;
251
+        }
252
+        return this._connect(jid, password);
253
+    }
220 254
 
221
-XMPP.prototype.connect = function (jid, password) {
222
-    if (!jid) {
223
-        var configDomain
224
-            = this.options.hosts.anonymousdomain || this.options.hosts.domain;
225
-        // Force authenticated domain if room is appended with '?login=true'
226
-        // or if we're joining with the token
227
-        if (this.options.hosts.anonymousdomain
228
-                && (window.location.search.indexOf("login=true") !== -1
229
-                    || this.options.token)) {
230
-            configDomain = this.options.hosts.domain;
255
+    createRoom (roomName, options, settings) {
256
+        // By default MUC nickname is the resource part of the JID
257
+        let mucNickname = Strophe.getNodeFromJid(this.connection.jid);
258
+        let roomjid = roomName  + "@" + this.options.hosts.muc + "/";
259
+        let cfgNickname
260
+            = (options.useNicks && options.nick) ? options.nick : null;
261
+
262
+        if (cfgNickname) {
263
+            // Use nick if it's defined
264
+            mucNickname = options.nick;
265
+        } else if (!this.authenticatedUser) {
266
+            // node of the anonymous JID is very long - here we trim it a bit
267
+            mucNickname = mucNickname.substr(0, 8);
231 268
         }
232
-        jid = configDomain || window.location.hostname;
269
+        // Constant JIDs need some random part to be appended in order to be
270
+        // able to join the MUC more than once.
271
+        if (this.authenticatedUser || cfgNickname != null) {
272
+            mucNickname += "-" + RandomUtil.randomHexString(6);
273
+        }
274
+
275
+        roomjid += mucNickname;
276
+
277
+        return this.connection.emuc.createRoom(roomjid, null, options,
278
+            settings);
233 279
     }
234
-    return this._connect(jid, password);
235
-};
236 280
 
237
-XMPP.prototype.createRoom = function (roomName, options, settings) {
238
-    var roomjid = roomName  + '@' + this.options.hosts.muc;
281
+    addListener (type, listener) {
282
+        this.eventEmitter.on(type, listener);
283
+    }
239 284
 
240
-    if (options.useNicks) {
241
-        if (options.nick) {
242
-            roomjid += '/' + options.nick;
243
-        } else {
244
-            roomjid += '/' + Strophe.getNodeFromJid(this.connection.jid);
245
-        }
246
-    } else {
247
-        var tmpJid = Strophe.getNodeFromJid(this.connection.jid);
285
+    removeListener (type, listener) {
286
+        this.eventEmitter.removeListener(type, listener);
287
+    }
248 288
 
249
-        if (!authenticatedUser)
250
-            tmpJid = tmpJid.substr(0, 8);
251
-        else
252
-            tmpJid += "-" + RandomUtil.randomHexString(6);
289
+    /**
290
+     * Sends 'data' as a log message to the focus. Returns true iff a message
291
+     * was sent.
292
+     * @param data
293
+     * @returns {boolean} true iff a message was sent.
294
+     */
295
+    sendLogs (data) {
296
+        if (!this.connection.emuc.focusMucJid)
297
+            return false;
298
+
299
+        const content = Base64.encode(
300
+            String.fromCharCode.apply(null,
301
+                Pako.deflateRaw(JSON.stringify(data))));
302
+        // XEP-0337-ish
303
+        const message = $msg({
304
+            to: this.connection.emuc.focusMucJid,
305
+            type: "normal"
306
+        });
307
+        message.c("log", {
308
+            xmlns: "urn:xmpp:eventlog",
309
+            id: "PeerConnectionStats"
310
+        });
311
+        message.c("message").t(content).up();
312
+        message.c("tag", {name: "deflated", value: "true"}).up();
313
+        message.up();
314
+
315
+        this.connection.send(message);
316
+        return true;
317
+    }
253 318
 
254
-        roomjid += '/' + tmpJid;
319
+    /**
320
+     * Returns the logs from strophe.jingle.
321
+     * @returns {Object}
322
+     */
323
+    getJingleLog () {
324
+        const jingle = this.connection.jingle;
325
+        return jingle? jingle.getLog() : {};
255 326
     }
256 327
 
257
-    return this.connection.emuc.createRoom(roomjid, null, options, settings);
258
-}
328
+    /**
329
+     * Returns the logs from strophe.
330
+     */
331
+    getXmppLog () {
332
+        return (this.connection.logger || {}).log || null;
333
+    }
259 334
 
260
-XMPP.prototype.addListener = function(type, listener) {
261
-    this.eventEmitter.on(type, listener);
262
-};
263
-
264
-XMPP.prototype.removeListener = function (type, listener) {
265
-    this.eventEmitter.removeListener(type, listener);
266
-};
267
-
268
-/**
269
- * Sends 'data' as a log message to the focus. Returns true iff a message
270
- * was sent.
271
- * @param data
272
- * @returns {boolean} true iff a message was sent.
273
- */
274
-XMPP.prototype.sendLogs = function (data) {
275
-    if (!this.connection.emuc.focusMucJid)
276
-        return false;
277
-
278
-    var deflate = true;
279
-
280
-    var content = JSON.stringify(data);
281
-    if (deflate) {
282
-        content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
335
+    dial (to, from, roomName,roomPass) {
336
+        this.connection.rayo.dial(to, from, roomName,roomPass);
283 337
     }
284
-    content = Base64.encode(content);
285
-    // XEP-0337-ish
286
-    var message = $msg({to: this.connection.emuc.focusMucJid, type: 'normal'});
287
-    message.c('log', {xmlns: 'urn:xmpp:eventlog', id: 'PeerConnectionStats'});
288
-    message.c('message').t(content).up();
289
-    if (deflate) {
290
-        message.c('tag', {name: "deflated", value: "true"}).up();
338
+
339
+    setMute (jid, mute) {
340
+        this.connection.moderate.setMute(jid, mute);
291 341
     }
292
-    message.up();
293
-
294
-    this.connection.send(message);
295
-    return true;
296
-};
297
-
298
-// Gets the logs from strophe.jingle.
299
-XMPP.prototype.getJingleLog = function () {
300
-    return this.connection.jingle ? this.connection.jingle.getLog() : {};
301
-};
302
-
303
-// Gets the logs from strophe.
304
-XMPP.prototype.getXmppLog = function () {
305
-    return this.connection.logger ? this.connection.logger.log : null;
306
-};
307
-
308
-XMPP.prototype.dial = function (to, from, roomName,roomPass) {
309
-    this.connection.rayo.dial(to, from, roomName,roomPass);
310
-};
311
-
312
-XMPP.prototype.setMute = function (jid, mute) {
313
-    this.connection.moderate.setMute(jid, mute);
314
-};
315
-
316
-XMPP.prototype.eject = function (jid) {
317
-    this.connection.moderate.eject(jid);
318
-};
319
-
320
-XMPP.prototype.getSessions = function () {
321
-    return this.connection.jingle.sessions;
322
-};
323
-
324
-/**
325
- * Disconnects this from the XMPP server (if this is connected).
326
- *
327
- * @param ev optionally, the event which triggered the necessity to disconnect
328
- * from the XMPP server (e.g. beforeunload, unload)
329
- */
330
-XMPP.prototype.disconnect = function (ev) {
331
-    if (this.disconnectInProgress
332
-            || !this.connection
333
-            || !this.connection.connected) {
334
-        this.eventEmitter.emit(JitsiConnectionEvents.WRONG_STATE);
335
-        return;
342
+
343
+    eject (jid) {
344
+        this.connection.moderate.eject(jid);
336 345
     }
337 346
 
338
-    this.disconnectInProgress = true;
339
-
340
-    // XXX Strophe is asynchronously sending by default. Unfortunately, that
341
-    // means that there may not be enough time to send an unavailable presence
342
-    // or disconnect at all. Switching Strophe to synchronous sending is not
343
-    // much of an option because it may lead to a noticeable delay in navigating
344
-    // away from the current location. As a compromise, we will try to increase
345
-    // the chances of sending an unavailable presence and/or disconecting within
346
-    // the short time span that we have upon unloading by invoking flush() on
347
-    // the connection. We flush() once before disconnect() in order to attemtp
348
-    // to have its unavailable presence at the top of the send queue. We flush()
349
-    // once more after disconnect() in order to attempt to have its unavailable
350
-    // presence sent as soon as possible.
351
-    this.connection.flush();
352
-
353
-    if (ev !== null && typeof ev !== 'undefined') {
354
-        var evType = ev.type;
355
-
356
-        if (evType == 'beforeunload' || evType == 'unload') {
357
-            // XXX Whatever we said above, synchronous sending is the best
358
-            // (known) way to properly disconnect from the XMPP server.
359
-            // Consequently, it may be fine to have the source code and comment
360
-            // it in or out depending on whether we want to run with it for some
361
-            // time.
362
-            this.connection.options.sync = true;
363
-        }
347
+    getSessions () {
348
+        return this.connection.jingle.sessions;
364 349
     }
365 350
 
366
-    this.connection.disconnect();
351
+    /**
352
+     * Disconnects this from the XMPP server (if this is connected).
353
+     *
354
+     * @param ev optionally, the event which triggered the necessity to disconnect
355
+     * from the XMPP server (e.g. beforeunload, unload)
356
+     */
357
+    disconnect (ev) {
358
+        if (this.disconnectInProgress
359
+                || !this.connection
360
+                || !this.connection.connected) {
361
+            this.eventEmitter.emit(JitsiConnectionEvents.WRONG_STATE);
362
+            return;
363
+        }
367 364
 
368
-    if (this.connection.options.sync !== true) {
365
+        this.disconnectInProgress = true;
366
+
367
+        // XXX Strophe is asynchronously sending by default. Unfortunately, that
368
+        // means that there may not be enough time to send an unavailable presence
369
+        // or disconnect at all. Switching Strophe to synchronous sending is not
370
+        // much of an option because it may lead to a noticeable delay in navigating
371
+        // away from the current location. As a compromise, we will try to increase
372
+        // the chances of sending an unavailable presence and/or disconecting within
373
+        // the short time span that we have upon unloading by invoking flush() on
374
+        // the connection. We flush() once before disconnect() in order to attemtp
375
+        // to have its unavailable presence at the top of the send queue. We flush()
376
+        // once more after disconnect() in order to attempt to have its unavailable
377
+        // presence sent as soon as possible.
369 378
         this.connection.flush();
379
+
380
+        if (ev !== null && typeof ev !== 'undefined') {
381
+            const evType = ev.type;
382
+
383
+            if (evType == 'beforeunload' || evType == 'unload') {
384
+                // XXX Whatever we said above, synchronous sending is the best
385
+                // (known) way to properly disconnect from the XMPP server.
386
+                // Consequently, it may be fine to have the source code and comment
387
+                // it in or out depending on whether we want to run with it for some
388
+                // time.
389
+                this.connection.options.sync = true;
390
+            }
391
+        }
392
+
393
+        this.connection.disconnect();
394
+
395
+        if (this.connection.options.sync !== true) {
396
+            this.connection.flush();
397
+        }
370 398
     }
371
-};
372 399
 
373
-module.exports = XMPP;
400
+    _initStrophePlugins() {
401
+        initEmuc(this);
402
+        initJingle(this, this.eventEmitter);
403
+        initStropheUtil();
404
+        initPing(this, this.eventEmitter);
405
+        initRayo();
406
+        initStropheLogger();
407
+    }
408
+}

+ 20
- 21
package.json View File

@@ -4,47 +4,46 @@
4 4
   "description": "JS library for accessing Jitsi server side deployments",
5 5
   "repository": {
6 6
     "type": "git",
7
-    "url": "git://github.com/jitsi/jitsi-meet"
7
+    "url": "git://github.com/jitsi/lib-jitsi-meet"
8 8
   },
9 9
   "keywords": [
10 10
     "jingle",
11 11
     "webrtc",
12 12
     "xmpp",
13
-    "browser"
13
+    "browser",
14
+    "jitsi"
14 15
   ],
15 16
   "author": "",
16 17
   "readmeFilename": "README.md",
17 18
   "dependencies": {
19
+    "async": "0.9.0",
20
+    "current-executing-script": "*",
18 21
     "events": "*",
22
+    "jitsi-meet-logger": "jitsi/jitsi-meet-logger",
23
+    "jssha": "1.5.0",
19 24
     "pako": "*",
25
+    "retry": "0.6.1",
20 26
     "sdp-interop": "0.1.11",
21
-    "sdp-transform": "1.5.*",
22 27
     "sdp-simulcast": "0.1.7",
23
-    "async": "0.9.0",
24
-    "retry": "0.6.1",
25
-    "jssha": "1.5.0",
26
-    "es6-promise": "*",
27
-    "jitsi-meet-logger": "jitsi/jitsi-meet-logger",
28
+    "sdp-transform": "1.5.*",
29
+    "socket.io-client": "1.4.5",
28 30
     "strophe": "^1.2.2",
29
-    "strophejs-plugins": "^0.0.6",
30
-    "socket.io-client": "1.3.6"
31
+    "strophejs-plugins": "^0.0.6"
31 32
   },
32 33
   "devDependencies": {
33
-    "browserify": "11.1.x",
34
+    "babel-core": "*",
35
+    "babel-loader": "*",
36
+    "babel-polyfill": "*",
37
+    "babel-preset-es2015": "*",
38
+    "eslint": "*",
34 39
     "jshint": "^2.8.0",
35 40
     "precommit-hook": "^3.0.0",
36
-    "exorcist": "*",
37
-    "uglify-js": "2.4.24",
38
-    "watchify": "^3.7.0"
41
+    "string-replace-loader": "*",
42
+    "webpack": "*"
39 43
   },
40 44
   "scripts": {
41
-    "install": "npm run browserify && npm run version && npm run uglifyjs",
42
-
43
-    "browserify": "browserify -d JitsiMeetJS.js -s JitsiMeetJS | exorcist lib-jitsi-meet.js.map > lib-jitsi-meet.js && [ -s lib-jitsi-meet.js ]",
44
-    "version": "VERSION=`./get-version.sh` && echo lib-jitsi-meet version is:${VERSION} && sed -i'' -e s/{#COMMIT_HASH#}/${VERSION}/g lib-jitsi-meet.js",
45
-    "uglifyjs": "uglifyjs -p relative lib-jitsi-meet.js -o lib-jitsi-meet.min.js --source-map lib-jitsi-meet.min.map --in-source-map lib-jitsi-meet.js.map",
46
-    "watch": "watchify JitsiMeetJS.js -s JitsiMeetJS -o lib-jitsi-meet.js -v",
47
-    "lint": "jshint .",
45
+    "install": "webpack -p",
46
+    "lint": "jshint . && eslint .",
48 47
     "validate": "npm ls"
49 48
   },
50 49
   "pre-commit": [

+ 22
- 0
service/RTC/CameraFacingMode.js View File

@@ -0,0 +1,22 @@
1
+/* global module */
2
+/**
3
+ * The possible camera facing modes. For now support only 'user' and
4
+ * 'environment' because 'left' and 'right' are not used anywhere in our
5
+ * projects at the time of this writing. For more information please refer to
6
+ * https://w3c.github.io/mediacapture-main/getusermedia.html#def-constraint-facingMode
7
+ *
8
+ * @enum {string}
9
+ */
10
+var CameraFacingMode = {
11
+    /**
12
+     * The mode which specifies the environment-facing camera.
13
+     */
14
+    ENVIRONMENT: "environment",
15
+
16
+    /**
17
+     * The mode which specifies the user-facing camera.
18
+     */
19
+    USER: "user"
20
+};
21
+
22
+module.exports = CameraFacingMode;

+ 11
- 2
service/RTC/RTCEvents.js View File

@@ -1,14 +1,23 @@
1 1
 var RTCEvents = {
2 2
     RTC_READY: "rtc.ready",
3 3
     DATA_CHANNEL_OPEN: "rtc.data_channel_open",
4
+    ENDPOINT_CONN_STATUS_CHANGED: "rtc.endpoint_conn_status_changed",
4 5
     LASTN_CHANGED: "rtc.lastn_changed",
5
-    DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
6
+    DOMINANT_SPEAKER_CHANGED: "rtc.dominant_speaker_changed",
6 7
     LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
7 8
     AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
8 9
     TRACK_ATTACHED: "rtc.track_attached",
10
+    REMOTE_TRACK_MUTE: "rtc.remote_track_mute",
11
+    REMOTE_TRACK_UNMUTE: "rtc.remote_track_unmute",
9 12
     AUDIO_OUTPUT_DEVICE_CHANGED: "rtc.audio_output_device_changed",
10 13
     DEVICE_LIST_CHANGED: "rtc.device_list_changed",
11
-    DEVICE_LIST_AVAILABLE: "rtc.device_list_available"
14
+    DEVICE_LIST_AVAILABLE: "rtc.device_list_available",
15
+    /**
16
+     * Indicates that a message from another participant is received on
17
+     * data channel.
18
+     */
19
+    ENDPOINT_MESSAGE_RECEIVED:
20
+        "rtc.endpoint_message_received"
12 21
 };
13 22
 
14 23
 module.exports = RTCEvents;

+ 0
- 7
service/connectionquality/CQEvents.js View File

@@ -1,7 +0,0 @@
1
-var CQEvents = {
2
-    LOCALSTATS_UPDATED: "cq.localstats_updated",
3
-    REMOTESTATS_UPDATED: "cq.remotestats_updated",
4
-    STOP: "cq.stop"
5
-};
6
-
7
-module.exports = CQEvents;

+ 10
- 0
service/connectivity/ConnectionQualityEvents.js View File

@@ -0,0 +1,10 @@
1
+/**
2
+ * Indicates that the local connection statistics were updated.
3
+ */
4
+export const LOCAL_STATS_UPDATED = "cq.local_stats_updated";
5
+
6
+/**
7
+ * Indicates that the connection statistics for a particular remote participant
8
+ * were updated.
9
+ */
10
+export const REMOTE_STATS_UPDATED = "cq.remote_stats_updated";

+ 25
- 14
service/statistics/Events.js View File

@@ -1,14 +1,25 @@
1
-module.exports = {
2
-    /**
3
-     * An event carrying connection statistics.
4
-     */
5
-    CONNECTION_STATS: "statistics.connectionstats",
6
-    /**
7
-     * FIXME: needs documentation.
8
-     */
9
-    AUDIO_LEVEL: "statistics.audioLevel",
10
-    /**
11
-     * Notifies about audio problem with remote participant.
12
-     */
13
-    AUDIO_NOT_WORKING: "statistics.audio_not_working"
14
-};
1
+/**
2
+ * Notifies about audio level in RTP statistics by SSRC.
3
+ *
4
+ * @param ssrc - The synchronization source identifier (SSRC) of the
5
+ * endpoint/participant whose audio level is being reported.
6
+ * @param {number} audioLevel - The audio level of <tt>ssrc</tt> according to
7
+ * RTP statistics.
8
+ * @param {boolean} isLocal - <tt>true</tt> if <tt>ssrc</tt> identifies the
9
+ * local endpoint/participant; otherwise, <tt>false</tt>.
10
+ */
11
+export const AUDIO_LEVEL = "statistics.audioLevel";
12
+
13
+/**
14
+ * An event carrying all statistics by ssrc.
15
+ */
16
+export const BYTE_SENT_STATS = "statistics.byte_sent_stats";
17
+
18
+/**
19
+ * An event carrying connection statistics.
20
+ *
21
+ * @param {object} connectionStats - The connection statistics carried by the
22
+ * event such as <tt>bandwidth</tt>, <tt>bitrate</tt>, <tt>packetLoss</tt>,
23
+ * <tt>resolution</tt>, and <tt>transport</tt>.
24
+ */
25
+export const CONNECTION_STATS = "statistics.connectionstats";

+ 25
- 2
service/xmpp/XMPPEvents.js View File

@@ -11,6 +11,11 @@ var XMPPEvents = {
11 11
     // Designates an event indicating that an offer (e.g. Jingle
12 12
     // session-initiate) was received.
13 13
     CALL_INCOMING: "xmpp.callincoming.jingle",
14
+    // Triggered when Jicofo kills our media session, this can happen while
15
+    // we're still in the MUC, when it decides to terminate the media session.
16
+    // For example when the session is idle for too long, because we're the only
17
+    // person in the conference room.
18
+    CALL_ENDED: "xmpp.callended.jingle",
14 19
     CHAT_ERROR_RECEIVED: "xmpp.chat_error_received",
15 20
     CONFERENCE_SETUP_FAILED: "xmpp.conference_setup_failed",
16 21
     // Designates an event indicating that the connection to the XMPP server
@@ -38,7 +43,6 @@ var XMPPEvents = {
38 43
     // Designates an event indicating that the display name of a participant
39 44
     // has changed.
40 45
     DISPLAY_NAME_CHANGED: "xmpp.display_name_changed",
41
-    DISPOSE_CONFERENCE: "xmpp.dispose_conference",
42 46
     ETHERPAD: "xmpp.etherpad",
43 47
     FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
44 48
     FOCUS_LEFT: "xmpp.focus_left",
@@ -76,9 +80,13 @@ var XMPPEvents = {
76 80
     MUC_MEMBER_JOINED: "xmpp.muc_member_joined",
77 81
     // Designates an event indicating that a participant left the XMPP MUC.
78 82
     MUC_MEMBER_LEFT: "xmpp.muc_member_left",
83
+    // Designates an event indicating that local participant left the muc
84
+    MUC_LEFT: "xmpp.muc_left",
79 85
     // Designates an event indicating that the MUC role of a participant has
80 86
     // changed.
81 87
     MUC_ROLE_CHANGED: "xmpp.muc_role_changed",
88
+    // Designates an event indicating that the MUC has been locked or unlocked.
89
+    MUC_LOCK_CHANGED: "xmpp.muc_lock_changed",
82 90
     // Designates an event indicating that a participant in the XMPP MUC has
83 91
     // advertised that they have audio muted (or unmuted).
84 92
     PARTICIPANT_AUDIO_MUTED: "xmpp.audio_muted",
@@ -128,6 +136,7 @@ var XMPPEvents = {
128 136
     REMOTE_TRACK_REMOVED: "xmpp.remote_track_removed",
129 137
     RESERVATION_ERROR: "xmpp.room_reservation_error",
130 138
     ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
139
+    ROOM_CONNECT_NOT_ALLOWED_ERROR: 'xmpp.room_connect_error.not_allowed',
131 140
     ROOM_JOIN_ERROR: 'xmpp.room_join_error',
132 141
     /**
133 142
      * Indicates that max users limit has been reached.
@@ -139,6 +148,17 @@ var XMPPEvents = {
139 148
      * Indicates that the local sendrecv streams in local SDP are changed.
140 149
      */
141 150
     SENDRECV_STREAMS_CHANGED: "xmpp.sendrecv_streams_changed",
151
+    /**
152
+     * Event fired when we do not get our 'session-accept' acknowledged by
153
+     * Jicofo. It most likely means that there is serious problem with our
154
+     * connection or XMPP server and we should reload the conference.
155
+     *
156
+     * We have seen that to happen in BOSH requests race condition when the BOSH
157
+     * request table containing the 'session-accept' was discarded by Prosody.
158
+     * Jicofo does send the RESULT immediately without any condition, so missing
159
+     * packets means that most likely it has never seen our IQ.
160
+     */
161
+    SESSION_ACCEPT_TIMEOUT: "xmpp.session_accept_timeout",
142 162
     // TODO: only used in a hack, should probably be removed.
143 163
     SET_LOCAL_DESCRIPTION_ERROR: 'xmpp.set_local_description_error',
144 164
 
@@ -163,6 +183,9 @@ var XMPPEvents = {
163 183
     LOCAL_UFRAG_CHANGED: "xmpp.local_ufrag_changed",
164 184
     // Designates an event indicating that the local ICE username fragment of
165 185
     // the jingle session has changed.
166
-    REMOTE_UFRAG_CHANGED: "xmpp.remote_ufrag_changed"
186
+    REMOTE_UFRAG_CHANGED: "xmpp.remote_ufrag_changed",
187
+    // Designates an event indicating that the local ICE connection state has
188
+    // changed.
189
+    ICE_CONNECTION_STATE_CHANGED: "xmpp.ice_connection_state_changed"
167 190
 };
168 191
 module.exports = XMPPEvents;

+ 64
- 0
webpack.config.js View File

@@ -0,0 +1,64 @@
1
+/* global __dirname */
2
+
3
+var child_process = require('child_process'); // eslint-disable-line camelcase
4
+var process = require('process');
5
+
6
+var minimize
7
+    = process.argv.indexOf('-p') !== -1
8
+        || process.argv.indexOf('--optimize-minimize') !== -1;
9
+
10
+module.exports = {
11
+    devtool: 'source-map',
12
+    entry: {
13
+        'lib-jitsi-meet': './JitsiMeetJS.js'
14
+    },
15
+    module: {
16
+        loaders: [ {
17
+            // Version this build of the lib-jitsi-meet library.
18
+
19
+            loader: 'string-replace',
20
+            query: {
21
+                flags: 'g',
22
+                replace:
23
+                    child_process.execSync( // eslint-disable-line camelcase
24
+                            __dirname + '/get-version.sh')
25
+
26
+                        // The type of the return value of
27
+                        // child_process.execSync is either Buffer or String.
28
+                        .toString()
29
+
30
+                            // Shells may automatically append CR and/or LF
31
+                            // characters to the output.
32
+                            .trim(),
33
+                search: '{#COMMIT_HASH#}'
34
+            },
35
+            test: __dirname + '/JitsiMeetJS.js'
36
+        }, {
37
+            // Transpile ES2015 (aka ES6) to ES5.
38
+
39
+            exclude: [
40
+                __dirname + '/modules/RTC/adapter.screenshare.js',
41
+                __dirname + '/node_modules/'
42
+            ],
43
+            loader: 'babel',
44
+            query: {
45
+                presets: [
46
+                    'es2015'
47
+                ]
48
+            },
49
+            test: /\.js$/
50
+        } ]
51
+    },
52
+    node: {
53
+        // Allow the use of the real filename of the module being executed. By
54
+        // default Webpack does not leak path-related information and provides a
55
+        // value that is a mock (/index.js).
56
+        __filename: true
57
+    },
58
+    output: {
59
+        filename: '[name]' + (minimize ? '.min' : '') + '.js',
60
+        library: 'JitsiMeetJS',
61
+        libraryTarget: 'umd',
62
+        sourceMapFilename: '[name].' + (minimize ? 'min' : 'js') + '.map'
63
+    }
64
+};

Loading…
Cancel
Save