Просмотр исходного кода

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 лет назад
Родитель
Сommit
f8352fdf4c
74 измененных файлов: 10714 добавлений и 3570 удалений
  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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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

+ 11
- 11
.jshintrc Просмотреть файл

1
 {
1
 {
2
     // Refer to http://jshint.com/docs/options/ for an exhaustive list of options
2
     // Refer to http://jshint.com/docs/options/ for an exhaustive list of options
3
     "asi": false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
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
     "curly": false, // true: Require {} for every new block or scope
5
     "curly": false, // true: Require {} for every new block or scope
6
+    "esversion": 6,
7
     "evil": true, // true: Tolerate use of `eval` and `new Function()`
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
     "indent": 4, // {int} Number of spaces to use for indentation
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
     "latedef": false, //This option prohibits the use of a variable before it was defined
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
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 69
- 66
JitsiConferenceErrors.js Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

1
 var JitsiConference = require("./JitsiConference");
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
  * Creates new connection object for the Jitsi Meet server side video conferencing service. Provides access to the
7
  * Creates new connection object for the Jitsi Meet server side video conferencing service. Provides access to the
15
     this.options = options;
17
     this.options = options;
16
     this.xmpp = new XMPP(options, token);
18
     this.xmpp = new XMPP(options, token);
17
     this.conferences = {};
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
         options = {};
48
         options = {};
28
 
49
 
29
     this.xmpp.connect(options.id, options.password);
50
     this.xmpp.connect(options.id, options.password);
30
-}
51
+};
31
 
52
 
32
 /**
53
 /**
33
  * Attach to existing connection. Can be used for optimizations. For example:
54
  * Attach to existing connection. Can be used for optimizations. For example:
38
  */
59
  */
39
 JitsiConnection.prototype.attach = function (options) {
60
 JitsiConnection.prototype.attach = function (options) {
40
     this.xmpp.attach(options);
61
     this.xmpp.attach(options);
41
-}
62
+};
42
 
63
 
43
 /**
64
 /**
44
  * Disconnect the client from the server.
65
  * Disconnect the client from the server.
51
     var x = this.xmpp;
72
     var x = this.xmpp;
52
 
73
 
53
     x.disconnect.apply(x, arguments);
74
     x.disconnect.apply(x, arguments);
54
-}
75
+};
55
 
76
 
56
 /**
77
 /**
57
  * This method allows renewal of the tokens if they are expiring.
78
  * This method allows renewal of the tokens if they are expiring.
59
  */
80
  */
60
 JitsiConnection.prototype.setToken = function (token) {
81
 JitsiConnection.prototype.setToken = function (token) {
61
     this.token = token;
82
     this.token = token;
62
-}
83
+};
63
 
84
 
64
 /**
85
 /**
65
  * Creates and joins new conference.
86
  * Creates and joins new conference.
74
         = new JitsiConference({name: name, config: options, connection: this});
95
         = new JitsiConference({name: name, config: options, connection: this});
75
     this.conferences[name] = conference;
96
     this.conferences[name] = conference;
76
     return conference;
97
     return conference;
77
-}
98
+};
78
 
99
 
79
 /**
100
 /**
80
  * Subscribes the passed listener to the event.
101
  * Subscribes the passed listener to the event.
83
  */
104
  */
84
 JitsiConnection.prototype.addEventListener = function (event, listener) {
105
 JitsiConnection.prototype.addEventListener = function (event, listener) {
85
     this.xmpp.addListener(event, listener);
106
     this.xmpp.addListener(event, listener);
86
-}
107
+};
87
 
108
 
88
 /**
109
 /**
89
  * Unsubscribes the passed handler.
110
  * Unsubscribes the passed handler.
92
  */
113
  */
93
 JitsiConnection.prototype.removeEventListener = function (event, listener) {
114
 JitsiConnection.prototype.removeEventListener = function (event, listener) {
94
     this.xmpp.removeListener(event, listener);
115
     this.xmpp.removeListener(event, listener);
95
-}
116
+};
96
 
117
 
97
 /**
118
 /**
98
  * Returns measured connectionTimes.
119
  * Returns measured connectionTimes.

+ 13
- 18
JitsiConnectionErrors.js Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

2
 var RTCEvents = require('./service/RTC/RTCEvents');
2
 var RTCEvents = require('./service/RTC/RTCEvents');
3
 var RTC = require("./modules/RTC/RTC");
3
 var RTC = require("./modules/RTC/RTC");
4
 var MediaType = require('./service/RTC/MediaType');
4
 var MediaType = require('./service/RTC/MediaType');
5
-var JitsiMediaDevicesEvents = require('./JitsiMediaDevicesEvents');
5
+import * as JitsiMediaDevicesEvents from "./JitsiMediaDevicesEvents";
6
 var Statistics = require("./modules/statistics/statistics");
6
 var Statistics = require("./modules/statistics/statistics");
7
 
7
 
8
 var eventEmitter = new EventEmitter();
8
 var eventEmitter = new EventEmitter();
46
     },
46
     },
47
     /**
47
     /**
48
      * Checks if its possible to enumerate available cameras/micropones.
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
     isDeviceListAvailable: function () {
53
     isDeviceListAvailable: function () {
52
         return RTC.isDeviceListAvailable();
54
         return RTC.isDeviceListAvailable();
128
      * Emits an event.
130
      * Emits an event.
129
      * @param {string} event - event name
131
      * @param {string} event - event name
130
      */
132
      */
131
-    emitEvent: function (event) {
133
+    emitEvent: function (event) { // eslint-disable-line no-unused-vars
132
         eventEmitter.emit.apply(eventEmitter, arguments);
134
         eventEmitter.emit.apply(eventEmitter, arguments);
133
     }
135
     }
134
 };
136
 };
135
 
137
 
136
-module.exports = JitsiMediaDevices;
138
+module.exports = JitsiMediaDevices;

+ 21
- 24
JitsiMediaDevicesEvents.js Просмотреть файл

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 Просмотреть файл

2
 var AuthUtil = require("./modules/util/AuthUtil");
2
 var AuthUtil = require("./modules/util/AuthUtil");
3
 var JitsiConnection = require("./JitsiConnection");
3
 var JitsiConnection = require("./JitsiConnection");
4
 var JitsiMediaDevices = require("./JitsiMediaDevices");
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
 var JitsiRecorderErrors = require("./JitsiRecorderErrors");
14
 var JitsiRecorderErrors = require("./JitsiRecorderErrors");
14
 var Logger = require("jitsi-meet-logger");
15
 var Logger = require("jitsi-meet-logger");
15
 var MediaType = require("./service/RTC/MediaType");
16
 var MediaType = require("./service/RTC/MediaType");
31
     var order = Resolutions[resolution].order;
32
     var order = Resolutions[resolution].order;
32
     var res = null;
33
     var res = null;
33
     var resName = null;
34
     var resName = null;
34
-    for(var i in Resolutions) {
35
+    for(let i in Resolutions) {
35
         var tmp = Resolutions[i];
36
         var tmp = Resolutions[i];
36
         if (!res || (res.order < tmp.order && tmp.order < order)) {
37
         if (!res || (res.order < tmp.order && tmp.order < order)) {
37
             resName = i;
38
             resName = i;
41
     return resName;
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
  * Namespace for the interface of Jitsi Meet Library.
69
  * Namespace for the interface of Jitsi Meet Library.
46
  */
70
  */
53
         conference: JitsiConferenceEvents,
77
         conference: JitsiConferenceEvents,
54
         connection: JitsiConnectionEvents,
78
         connection: JitsiConnectionEvents,
55
         track: JitsiTrackEvents,
79
         track: JitsiTrackEvents,
56
-        mediaDevices: JitsiMediaDevicesEvents
80
+        mediaDevices: JitsiMediaDevicesEvents,
81
+        connectionQuality: ConnectionQualityEvents
57
     },
82
     },
58
     errors: {
83
     errors: {
59
         conference: JitsiConferenceErrors,
84
         conference: JitsiConferenceErrors,
66
     },
91
     },
67
     logLevels: Logger.levels,
92
     logLevels: Logger.levels,
68
     mediaDevices: JitsiMediaDevices,
93
     mediaDevices: JitsiMediaDevices,
94
+    analytics: null,
69
     init: function (options) {
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
         if (options.enableWindowOnErrorHandler) {
100
         if (options.enableWindowOnErrorHandler) {
77
             GlobalOnErrorHandler.addHandler(
101
             GlobalOnErrorHandler.addHandler(
78
                 this.getGlobalOnErrorHandler.bind(this));
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
         if (window.jitsiRegionInfo
106
         if (window.jitsiRegionInfo
83
             && Object.keys(window.jitsiRegionInfo).length > 0) {
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
         return RTC.init(options || {});
128
         return RTC.init(options || {});
93
     },
129
     },
112
      * will be returned trough the Promise, otherwise JitsiTrack objects will be returned.
148
      * will be returned trough the Promise, otherwise JitsiTrack objects will be returned.
113
      * @param {string} options.cameraDeviceId
149
      * @param {string} options.cameraDeviceId
114
      * @param {string} options.micDeviceId
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
      * @param {boolean} (firePermissionPromptIsShownEvent) - if event
173
      * @param {boolean} (firePermissionPromptIsShownEvent) - if event
116
      *      JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN should be fired
174
      *      JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN should be fired
117
      * @returns {Promise.<{Array.<JitsiTrack>}, JitsiConferenceError>}
175
      * @returns {Promise.<{Array.<JitsiTrack>}, JitsiConferenceError>}
124
         if (firePermissionPromptIsShownEvent === true) {
182
         if (firePermissionPromptIsShownEvent === true) {
125
             window.setTimeout(function () {
183
             window.setTimeout(function () {
126
                 if (!promiseFulfilled) {
184
                 if (!promiseFulfilled) {
127
-                    var browser = RTCBrowserType.getBrowserType()
128
-                        .split('rtc_browser.')[1];
129
-
130
-                    if (RTCBrowserType.isAndroid()) {
131
-                        browser = 'android';
132
-                    }
133
-
134
                     JitsiMediaDevices.emitEvent(
185
                     JitsiMediaDevices.emitEvent(
135
                         JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
186
                         JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
136
-                        browser);
187
+                        RTCBrowserType.getBrowserName());
137
                 }
188
                 }
138
             }, USER_MEDIA_PERMISSION_PROMPT_TIMEOUT);
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
         return RTC.obtainAudioAndVideoPermissions(options || {})
197
         return RTC.obtainAudioAndVideoPermissions(options || {})
142
             .then(function(tracks) {
198
             .then(function(tracks) {
143
                 promiseFulfilled = true;
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
                 if(!RTC.options.disableAudioLevels)
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
                         var mStream = track.getOriginalStream();
210
                         var mStream = track.getOriginalStream();
149
                         if(track.getType() === MediaType.AUDIO){
211
                         if(track.getType() === MediaType.AUDIO){
150
                             Statistics.startLocalStats(mStream,
212
                             Statistics.startLocalStats(mStream,
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
                 return tracks;
233
                 return tracks;
161
             }).catch(function (error) {
234
             }).catch(function (error) {
162
                 promiseFulfilled = true;
235
                 promiseFulfilled = true;
171
                         logger.debug("Retry createLocalTracks with resolution",
244
                         logger.debug("Retry createLocalTracks with resolution",
172
                             newResolution);
245
                             newResolution);
173
 
246
 
247
+                        Statistics.analytics.sendEvent(
248
+                            "getUserMedia.fail.resolution." + oldResolution);
249
+
174
                         return LibJitsiMeet.createLocalTracks(options);
250
                         return LibJitsiMeet.createLocalTracks(options);
175
                     }
251
                     }
176
                 }
252
                 }
180
                     // User cancelled action is not really an error, so only
256
                     // User cancelled action is not really an error, so only
181
                     // log it as an event to avoid having conference classified
257
                     // log it as an event to avoid having conference classified
182
                     // as partially failed
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
                 } else {
276
                 } else {
185
                     // Report gUM failed to the stats
277
                     // Report gUM failed to the stats
186
                     Statistics.sendGetUserMediaFailed(error);
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
                 return Promise.reject(error);
288
                 return Promise.reject(error);
190
             }.bind(this));
289
             }.bind(this));
191
     },
290
     },
192
     /**
291
     /**
193
      * Checks if its possible to enumerate available cameras/micropones.
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
      * @deprecated use JitsiMeetJS.mediaDevices.isDeviceListAvailable instead
296
      * @deprecated use JitsiMeetJS.mediaDevices.isDeviceListAvailable instead
196
      */
297
      */
197
     isDeviceListAvailable: function () {
298
     isDeviceListAvailable: function () {
249
     }
350
     }
250
 };
351
 };
251
 
352
 
252
-//Setups the promise object.
253
-window.Promise = window.Promise || require("es6-promise").Promise;
254
-
255
 module.exports = LibJitsiMeet;
353
 module.exports = LibJitsiMeet;

+ 228
- 179
JitsiParticipant.js Просмотреть файл

1
 /* global Strophe */
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 Просмотреть файл

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
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
5
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
6
     = "Video resolution is not supported: ";
6
     = "Video resolution is not supported: ";
22
     = "Constraint could not be satisfied: ";
22
     = "Constraint could not be satisfied: ";
23
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_IS_DISPOSED]
23
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_IS_DISPOSED]
24
     = "Track has been already disposed";
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
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_MUTE_UNMUTE_IN_PROGRESS]
27
 TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_MUTE_UNMUTE_IN_PROGRESS]
26
     = "Track mute/unmute process is currently in progress";
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
  * @extends Error
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
             case "PermissionDeniedError":
71
             case "PermissionDeniedError":
60
             case "SecurityError":
72
             case "SecurityError":
61
                 this.name = JitsiTrackErrors.PERMISSION_DENIED;
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
                         + (this.gum.devices || []).join(", ");
76
                         + (this.gum.devices || []).join(", ");
65
                 break;
77
                 break;
78
+            case "DevicesNotFoundError":
66
             case "NotFoundError":
79
             case "NotFoundError":
67
                 this.name = JitsiTrackErrors.NOT_FOUND;
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
                         + (this.gum.devices || []).join(", ");
83
                         + (this.gum.devices || []).join(", ");
71
                 break;
84
                 break;
72
             case "ConstraintNotSatisfiedError":
85
             case "ConstraintNotSatisfiedError":
73
             case "OverconstrainedError":
86
             case "OverconstrainedError":
74
                 var constraintName = error.constraintName;
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
                     this.name = JitsiTrackErrors.UNSUPPORTED_RESOLUTION;
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
                 } else {
104
                 } else {
91
                     this.name = JitsiTrackErrors.CONSTRAINT_FAILED;
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
                 break;
110
                 break;
97
             default:
111
             default:
98
                 this.name = JitsiTrackErrors.GENERAL;
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
                 break;
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
         } else {
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
  * Gets failed resolution constraint from corresponding object.
136
  * Gets failed resolution constraint from corresponding object.
124
  * @param {string} failedConstraintName
137
  * @param {string} failedConstraintName
127
  */
140
  */
128
 function getResolutionFromFailedConstraint(failedConstraintName, constraints) {
141
 function getResolutionFromFailedConstraint(failedConstraintName, constraints) {
129
     if (constraints && constraints.video && constraints.video.mandatory) {
142
     if (constraints && constraints.video && constraints.video.mandatory) {
130
-        if (failedConstraintName === "width") {
143
+        switch (failedConstraintName) {
144
+        case "width":
131
             return constraints.video.mandatory.minWidth;
145
             return constraints.video.mandatory.minWidth;
132
-        } else if (failedConstraintName === "height") {
146
+        case "height":
133
             return constraints.video.mandatory.minHeight;
147
             return constraints.video.mandatory.minHeight;
134
-        } else {
148
+        default:
135
             return constraints.video.mandatory[failedConstraintName] || "";
149
             return constraints.video.mandatory[failedConstraintName] || "";
136
         }
150
         }
137
     }
151
     }
138
 
152
 
139
     return "";
153
     return "";
140
 }
154
 }
141
-
142
-module.exports = JitsiTrackError;

+ 67
- 60
JitsiTrackErrors.js Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

20
  * callback is going to receive one parameter which is going to be JS error
20
  * callback is going to receive one parameter which is going to be JS error
21
  * object with a reason for failure in it.
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
     if (!window.XMLHttpRequest) {
27
     if (!window.XMLHttpRequest) {
26
         error_callback(new Error("XMLHttpRequest is not supported!"));
28
         error_callback(new Error("XMLHttpRequest is not supported!"));
27
         return;
29
         return;
40
                 try {
42
                 try {
41
                     var data = JSON.parse(xhttp.responseText);
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
                     window.jitsiRegionInfo = {
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
                     success_callback(data);
54
                     success_callback(data);
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
     xhttp.timeout = 3000;
70
     xhttp.timeout = 3000;
64
 
71
 
65
-    xhttp.open("GET", webserviceUrl, true);
66
     window.connectionTimes = {};
72
     window.connectionTimes = {};
67
     var now = window.connectionTimes["external_connect.sending"] =
73
     var now = window.connectionTimes["external_connect.sending"] =
68
         window.performance.now();
74
         window.performance.now();

+ 33
- 5
doc/API.md Просмотреть файл

50
     9. disableAudioLevels - boolean property. Enables/disables audio levels.
50
     9. disableAudioLevels - boolean property. Enables/disables audio levels.
51
     10. disableSimulcast - boolean property. Enables/disables simulcast.
51
     10. disableSimulcast - boolean property. Enables/disables simulcast.
52
     11. enableWindowOnErrorHandler - boolean property (default false). Enables/disables attaching global onerror handler (window.onerror).
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
 * ```JitsiMeetJS.JitsiConnection``` - the ```JitsiConnection``` constructor. You can use that to create new server connection.
58
 * ```JitsiMeetJS.JitsiConnection``` - the ```JitsiConnection``` constructor. You can use that to create new server connection.
55
 
59
 
118
         - 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)))
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
         - CONNECTION_STATS - New local connection statistics are received. (parameters - stats(object))
123
         - CONNECTION_STATS - New local connection statistics are received. (parameters - stats(object))
120
         - AUTH_STATUS_CHANGED - notifies that authentication is enabled or disabled, or local user authenticated (logged in). (parameters - isAuthEnabled(boolean), authIdentity(string))
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
     2. connection
128
     2. connection
123
         - CONNECTION_FAILED - indicates that the server connection failed.
129
         - CONNECTION_FAILED - indicates that the server connection failed.
165
         - NOT_FOUND - getUserMedia-related error, indicates that requested device was not found.
171
         - NOT_FOUND - getUserMedia-related error, indicates that requested device was not found.
166
         - CONSTRAINT_FAILED - getUserMedia-related error, indicates that some of requested constraints in getUserMedia call were not satisfied.
172
         - CONSTRAINT_FAILED - getUserMedia-related error, indicates that some of requested constraints in getUserMedia call were not satisfied.
167
         - TRACK_IS_DISPOSED - an error which indicates that track has been already disposed and cannot be longer used.
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
         - TRACK_MUTE_UNMUTE_IN_PROGRESS - an error which indicates that track is currently in progress of muting or unmuting itself.
175
         - TRACK_MUTE_UNMUTE_IN_PROGRESS - an error which indicates that track is currently in progress of muting or unmuting itself.
169
         - CHROME_EXTENSION_GENERIC_ERROR - generic error for jidesha extension for Chrome.
176
         - CHROME_EXTENSION_GENERIC_ERROR - generic error for jidesha extension for Chrome.
170
         - CHROME_EXTENSION_USER_CANCELED - an error which indicates that user canceled screen sharing window selection dialog in jidesha extension for Chrome.
177
         - CHROME_EXTENSION_USER_CANCELED - an error which indicates that user canceled screen sharing window selection dialog in jidesha extension for Chrome.
171
         - CHROME_EXTENSION_INSTALLATION_ERROR - an error which indicates that the jidesha extension for Chrome is failed to install.
178
         - CHROME_EXTENSION_INSTALLATION_ERROR - an error which indicates that the jidesha extension for Chrome is failed to install.
172
         - 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.
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
 * ```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:
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
     1. ```JitsiTrackError``` - Error that happened to a JitsiTrack.
182
     1. ```JitsiTrackError``` - Error that happened to a JitsiTrack.
176
-        
183
+
177
 * ```JitsiMeetJS.logLevels``` - object with the log levels:
184
 * ```JitsiMeetJS.logLevels``` - object with the log levels:
178
     1. TRACE
185
     1. TRACE
179
     2. DEBUG
186
     2. DEBUG
198
             - muc
205
             - muc
199
             - anonymousdomain
206
             - anonymousdomain
200
         3. useStunTurn -
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
 2. connect(options) - establish server connection
210
 2. connect(options) - establish server connection
203
     - options - JS Object with ```id``` and ```password``` properties.
211
     - options - JS Object with ```id``` and ```password``` properties.
212
         3. jirecon
220
         3. jirecon
213
         4. callStatsID - callstats credentials
221
         4. callStatsID - callstats credentials
214
         5. callStatsSecret - callstats credentials
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
         **NOTE: if 4 and 5 are set the library is going to send events to callstats. Otherwise the callstats integration will be disabled.**
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
 5. addEventListener(event, listener) - Subscribes the passed listener to the event.
226
 5. addEventListener(event, listener) - Subscribes the passed listener to the event.
341
 
348
 
342
     Note: available only for moderator
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
 JitsiTrack
372
 JitsiTrack
345
 ======
373
 ======
346
 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).
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
 
409
 
382
 JitsiTrackError
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
 so ```"name"```, ```"message"``` and ```"stack"``` properties are available. For GUM-related errors,
413
 so ```"name"```, ```"message"``` and ```"stack"``` properties are available. For GUM-related errors,
386
 exposes additional ```"gum"``` property, which is an object with following properties:
414
 exposes additional ```"gum"``` property, which is an object with following properties:
387
  - error - original GUM error
415
  - error - original GUM error

+ 14
- 13
doc/example/example.js Просмотреть файл

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

+ 79
- 43
modules/RTC/DataChannels.js Просмотреть файл

1
-/* global config, APP, Strophe */
2
-
3
 // cache datachannels to avoid garbage collection
1
 // cache datachannels to avoid garbage collection
4
 // https://code.google.com/p/chromium/issues/detail?id=405545
2
 // https://code.google.com/p/chromium/issues/detail?id=405545
5
 
3
 
7
 var RTCEvents = require("../../service/RTC/RTCEvents");
5
 var RTCEvents = require("../../service/RTC/RTCEvents");
8
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
6
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
9
 
7
 
10
-
11
 /**
8
 /**
12
  * Binds "ondatachannel" event listener to given PeerConnection instance.
9
  * Binds "ondatachannel" event listener to given PeerConnection instance.
13
  * @param peerConnection WebRTC peer connection instance.
10
  * @param peerConnection WebRTC peer connection instance.
40
      var msgData = event.data;
37
      var msgData = event.data;
41
      logger.info("Got My Data Channel Message:", msgData, dataChannel);
38
      logger.info("Got My Data Channel Message:", msgData, dataChannel);
42
      };*/
39
      };*/
43
-};
44
-
40
+}
45
 
41
 
46
 /**
42
 /**
47
  * Callback triggered by PeerConnection when new data channel is opened
43
  * Callback triggered by PeerConnection when new data channel is opened
51
 DataChannels.prototype.onDataChannel = function (event) {
47
 DataChannels.prototype.onDataChannel = function (event) {
52
     var dataChannel = event.channel;
48
     var dataChannel = event.channel;
53
     var self = this;
49
     var self = this;
54
-    var selectedEndpoint = null;
55
 
50
 
56
     dataChannel.onopen = function () {
51
     dataChannel.onopen = function () {
57
         logger.info("Data channel opened by the Videobridge!", dataChannel);
52
         logger.info("Data channel opened by the Videobridge!", dataChannel);
63
         //dataChannel.send(new ArrayBuffer(12));
58
         //dataChannel.send(new ArrayBuffer(12));
64
 
59
 
65
         self.eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
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
     dataChannel.onerror = function (error) {
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
         logger.error("Data Channel Error:", error, dataChannel);
69
         logger.error("Data Channel Error:", error, dataChannel);
79
     };
70
     };
80
 
71
 
104
                 logger.info(
95
                 logger.info(
105
                     "Data channel new dominant speaker event: ",
96
                     "Data channel new dominant speaker event: ",
106
                     dominantSpeakerEndpoint);
97
                     dominantSpeakerEndpoint);
107
-                self.eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
98
+                self.eventEmitter.emit(RTCEvents.DOMINANT_SPEAKER_CHANGED,
99
+                  dominantSpeakerEndpoint);
108
             }
100
             }
109
             else if ("InLastNChangeEvent" === colibriClass) {
101
             else if ("InLastNChangeEvent" === colibriClass) {
110
                 var oldValue = obj.oldValue;
102
                 var oldValue = obj.oldValue;
143
                     lastNEndpoints, endpointsEnteringLastN, obj);
135
                     lastNEndpoints, endpointsEnteringLastN, obj);
144
                 self.eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
136
                 self.eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
145
                     lastNEndpoints, endpointsEnteringLastN, obj);
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
             else {
151
             else {
148
                 logger.debug("Data channel JSON-formatted message: ", obj);
152
                 logger.debug("Data channel JSON-formatted message: ", obj);
176
 
180
 
177
 /**
181
 /**
178
  * Sends a "selected endpoint changed" message via the data channel.
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
 DataChannels.prototype.sendSelectedEndpointMessage = function (endpointId) {
188
 DataChannels.prototype.sendSelectedEndpointMessage = function (endpointId) {
181
-    this.selectedEndpoint = endpointId;
182
     this._onXXXEndpointChanged("selected", endpointId);
189
     this._onXXXEndpointChanged("selected", endpointId);
183
 };
190
 };
184
 
191
 
185
 /**
192
 /**
186
  * Sends a "pinned endpoint changed" message via the data channel.
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
 DataChannels.prototype.sendPinnedEndpointMessage = function (endpointId) {
199
 DataChannels.prototype.sendPinnedEndpointMessage = function (endpointId) {
189
-    this._onXXXEndpointChanged("pinnned", endpointId);
200
+    this._onXXXEndpointChanged("pinned", endpointId);
190
 };
201
 };
191
 
202
 
192
 /**
203
 /**
193
  * Notifies Videobridge about a change in the value of a specific
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
  * @param xxx the name of the endpoint-related property whose value changed
207
  * @param xxx the name of the endpoint-related property whose value changed
197
  * @param userResource the new value of the endpoint-related property after the
208
  * @param userResource the new value of the endpoint-related property after the
198
  * change
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
 DataChannels.prototype._onXXXEndpointChanged = function (xxx, userResource) {
214
 DataChannels.prototype._onXXXEndpointChanged = function (xxx, userResource) {
201
     // Derive the correct words from xxx such as selected and Selected, pinned
215
     // Derive the correct words from xxx such as selected and Selected, pinned
204
     var tail = xxx.substring(1);
218
     var tail = xxx.substring(1);
205
     var lower = head.toLowerCase() + tail;
219
     var lower = head.toLowerCase() + tail;
206
     var upper = head.toUpperCase() + tail;
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
 DataChannels.prototype._some = function (callback, thisArg) {
238
 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
 module.exports = DataChannels;
286
 module.exports = DataChannels;

+ 401
- 148
modules/RTC/JitsiLocalTrack.js Просмотреть файл

1
 /* global __filename, Promise */
1
 /* global __filename, Promise */
2
-var logger = require("jitsi-meet-logger").getLogger(__filename);
2
+var CameraFacingMode = require('../../service/RTC/CameraFacingMode');
3
 var JitsiTrack = require("./JitsiTrack");
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
 var RTCBrowserType = require("./RTCBrowserType");
9
 var RTCBrowserType = require("./RTCBrowserType");
5
-var JitsiTrackEvents = require('../../JitsiTrackEvents');
6
-var JitsiTrackErrors = require("../../JitsiTrackErrors");
7
-var JitsiTrackError = require("../../JitsiTrackError");
8
 var RTCEvents = require("../../service/RTC/RTCEvents");
10
 var RTCEvents = require("../../service/RTC/RTCEvents");
9
 var RTCUtils = require("./RTCUtils");
11
 var RTCUtils = require("./RTCUtils");
12
+var Statistics = require("../statistics/statistics");
10
 var VideoType = require('../../service/RTC/VideoType');
13
 var VideoType = require('../../service/RTC/VideoType');
11
 
14
 
12
 /**
15
 /**
18
  * @param videoType the VideoType of the JitsiRemoteTrack
21
  * @param videoType the VideoType of the JitsiRemoteTrack
19
  * @param resolution the video resoultion if it's a video track
22
  * @param resolution the video resoultion if it's a video track
20
  * @param deviceId the ID of the local device for this track
23
  * @param deviceId the ID of the local device for this track
24
+ * @param facingMode the camera facing mode used in getUserMedia call
21
  * @constructor
25
  * @constructor
22
  */
26
  */
23
 function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
27
 function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
24
-                         deviceId) {
28
+                         deviceId, facingMode) {
25
     var self = this;
29
     var self = this;
26
 
30
 
27
     JitsiTrack.call(this,
31
     JitsiTrack.call(this,
35
         mediaType, videoType, null /* ssrc */);
39
         mediaType, videoType, null /* ssrc */);
36
     this.dontFireRemoveEvent = false;
40
     this.dontFireRemoveEvent = false;
37
     this.resolution = resolution;
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
     this.deviceId = deviceId;
48
     this.deviceId = deviceId;
39
     this.startMuted = false;
49
     this.startMuted = false;
40
-    this.disposed = false;
41
-    //FIXME: This dependacy is not necessary.
42
-    this.conference = null;
43
     this.initialMSID = this.getMSID();
50
     this.initialMSID = this.getMSID();
44
     this.inMuteOrUnmuteProgress = false;
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
     // Currently there is no way to know the MediaStreamTrack ended due to to
59
     // Currently there is no way to know the MediaStreamTrack ended due to to
47
     // device disconnect in Firefox through e.g. "readyState" property. Instead
60
     // device disconnect in Firefox through e.g. "readyState" property. Instead
48
     // we will compare current track's label with device labels from
61
     // we will compare current track's label with device labels from
49
     // enumerateDevices() list.
62
     // enumerateDevices() list.
50
     this._trackEnded = false;
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
     // Currently there is no way to determine with what device track was
77
     // Currently there is no way to determine with what device track was
53
     // created (until getConstraints() support), however we can associate tracks
78
     // created (until getConstraints() support), however we can associate tracks
54
     // with real devices obtained from enumerateDevices() call as soon as it's
79
     // with real devices obtained from enumerateDevices() call as soon as it's
55
     // called.
80
     // called.
56
     this._realDeviceId = this.deviceId === '' ? undefined : this.deviceId;
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
     this._onDeviceListChanged = function (devices) {
96
     this._onDeviceListChanged = function (devices) {
59
         self._setRealDeviceIdFromDeviceList(devices);
97
         self._setRealDeviceIdFromDeviceList(devices);
60
 
98
 
83
 
121
 
84
     RTCUtils.addListener(RTCEvents.DEVICE_LIST_CHANGED,
122
     RTCUtils.addListener(RTCEvents.DEVICE_LIST_CHANGED,
85
         this._onDeviceListChanged);
123
         this._onDeviceListChanged);
124
+
125
+    this._initNoDataFromSourceHandlers();
86
 }
126
 }
87
 
127
 
88
 JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype);
128
 JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype);
96
     return  this.getTrack().readyState === 'ended' || this._trackEnded;
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
  * Sets real device ID by comparing track information with device information.
202
  * Sets real device ID by comparing track information with device information.
101
  * This is temporary solution until getConstraints() method will be implemented
203
  * This is temporary solution until getConstraints() method will be implemented
134
 
236
 
135
 /**
237
 /**
136
  * Creates Promise for mute/unmute operation.
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
  * Mutes / unmutes the track.
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
  * will be unmuted.
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
     if (this.isMuted() === mute) {
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
         RTCBrowserType.isFirefox()) {
292
         RTCBrowserType.isFirefox()) {
196
-
197
-        if (this.track)
293
+        if(this.track)
198
             this.track.enabled = !mute;
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
     } else {
295
     } else {
204
-        if (mute) {
296
+        if(mute) {
205
             this.dontFireRemoveEvent = true;
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
         } else {
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
             var streamOptions = {
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
                 .then(function (streamsInfo) {
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
                     if(!streamInfo) {
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
  * Stops sending the media track. And removes it from the HTML.
436
  * Stops sending the media track. And removes it from the HTML.
289
  * NOTE: Works for local tracks only.
437
  * NOTE: Works for local tracks only.
438
+ *
439
+ * @extends JitsiTrack#dispose
290
  * @returns {Promise}
440
  * @returns {Promise}
291
  */
441
  */
292
 JitsiLocalTrack.prototype.dispose = function () {
442
 JitsiLocalTrack.prototype.dispose = function () {
443
+    var self = this;
293
     var promise = Promise.resolve();
444
     var promise = Promise.resolve();
294
 
445
 
295
     if (this.conference){
446
     if (this.conference){
297
     }
448
     }
298
 
449
 
299
     if (this.stream) {
450
     if (this.stream) {
300
-        RTCUtils.stopMediaStream(this.stream);
451
+        this._stopMediaStream();
301
         this.detach();
452
         this.detach();
302
     }
453
     }
303
 
454
 
304
-    this.disposed = true;
305
-
306
     RTCUtils.removeListener(RTCEvents.DEVICE_LIST_CHANGED,
455
     RTCUtils.removeListener(RTCEvents.DEVICE_LIST_CHANGED,
307
         this._onDeviceListChanged);
456
         this._onDeviceListChanged);
308
 
457
 
311
             this._onAudioOutputDeviceChanged);
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
     }
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
  * Updates the SSRC associated with the MediaStream in JitsiLocalTrack object.
487
  * Updates the SSRC associated with the MediaStream in JitsiLocalTrack object.
352
  * @ssrc the new ssrc
488
  * @ssrc the new ssrc
356
 };
492
 };
357
 
493
 
358
 
494
 
359
-//FIXME: This dependacy is not necessary. This is quick fix.
360
 /**
495
 /**
361
  * Sets the JitsiConference object associated with the track. This is temp
496
  * Sets the JitsiConference object associated with the track. This is temp
362
  * solution.
497
  * solution.
364
  */
499
  */
365
 JitsiLocalTrack.prototype._setConference = function(conference) {
500
 JitsiLocalTrack.prototype._setConference = function(conference) {
366
     this.conference = conference;
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
     return this._realDeviceId || this.deviceId;
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
 module.exports = JitsiLocalTrack;
654
 module.exports = JitsiLocalTrack;

+ 102
- 7
modules/RTC/JitsiRemoteTrack.js Просмотреть файл

1
+/* global Strophe */
2
+
1
 var JitsiTrack = require("./JitsiTrack");
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
  * Represents a single media track (either audio or video).
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
  * @param ownerJid the MUC JID of the track owner
16
  * @param ownerJid the MUC JID of the track owner
8
  * @param stream WebRTC MediaStream, parent of the track
17
  * @param stream WebRTC MediaStream, parent of the track
9
  * @param track underlying WebRTC MediaStreamTrack for new JitsiRemoteTrack
18
  * @param track underlying WebRTC MediaStreamTrack for new JitsiRemoteTrack
13
  * @param muted intial muted state of the JitsiRemoteTrack
22
  * @param muted intial muted state of the JitsiRemoteTrack
14
  * @constructor
23
  * @constructor
15
  */
24
  */
16
-function JitsiRemoteTrack(RTC, ownerJid, stream, track, mediaType, videoType,
25
+function JitsiRemoteTrack(rtc, conference, ownerJid, stream, track, mediaType, videoType,
17
                           ssrc, muted) {
26
                           ssrc, muted) {
18
     JitsiTrack.call(
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
     this.peerjid = ownerJid;
30
     this.peerjid = ownerJid;
22
     this.muted = muted;
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
 JitsiRemoteTrack.prototype = Object.create(JitsiTrack.prototype);
41
 JitsiRemoteTrack.prototype = Object.create(JitsiTrack.prototype);
26
 JitsiRemoteTrack.prototype.constructor = JitsiRemoteTrack;
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
  * Sets current muted status and fires an events for the change.
72
  * Sets current muted status and fires an events for the change.
30
  * @param value the muted status.
73
  * @param value the muted status.
33
     if(this.muted === value)
76
     if(this.muted === value)
34
         return;
77
         return;
35
 
78
 
79
+    if(value)
80
+        this.hasBeenMuted = true;
81
+
36
     // we can have a fake video stream
82
     // we can have a fake video stream
37
     if(this.stream)
83
     if(this.stream)
38
         this.stream.muted = value;
84
         this.stream.muted = value;
39
 
85
 
40
     this.muted = value;
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
     this.eventEmitter.emit(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, type);
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
 module.exports = JitsiRemoteTrack;
184
 module.exports = JitsiRemoteTrack;

+ 98
- 11
modules/RTC/JitsiTrack.js Просмотреть файл

1
 /* global __filename, module */
1
 /* global __filename, module */
2
 var logger = require("jitsi-meet-logger").getLogger(__filename);
2
 var logger = require("jitsi-meet-logger").getLogger(__filename);
3
 var RTCBrowserType = require("./RTCBrowserType");
3
 var RTCBrowserType = require("./RTCBrowserType");
4
-var RTCEvents = require("../../service/RTC/RTCEvents");
5
 var RTCUtils = require("./RTCUtils");
4
 var RTCUtils = require("./RTCUtils");
6
-var JitsiTrackEvents = require("../../JitsiTrackEvents");
5
+import * as JitsiTrackEvents from "../../JitsiTrackEvents";
7
 var EventEmitter = require("events");
6
 var EventEmitter = require("events");
8
 var MediaType = require("../../service/RTC/MediaType");
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
  * This implements 'onended' callback normally fired by WebRTC after the stream
19
  * This implements 'onended' callback normally fired by WebRTC after the stream
12
  * is stopped. There is no such behaviour yet in FF, so we have to add it.
20
  * is stopped. There is no such behaviour yet in FF, so we have to add it.
54
  * @param videoType the VideoType for this track if any
62
  * @param videoType the VideoType for this track if any
55
  * @param ssrc the SSRC of this track if known
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
                     videoType, ssrc)
66
                     videoType, ssrc)
59
 {
67
 {
60
     /**
68
     /**
62
      * @type {Array}
70
      * @type {Array}
63
      */
71
      */
64
     this.containers = [];
72
     this.containers = [];
65
-    this.rtc = rtc;
73
+    this.conference = conference;
66
     this.stream = stream;
74
     this.stream = stream;
67
     this.ssrc = ssrc;
75
     this.ssrc = ssrc;
68
     this.eventEmitter = new EventEmitter();
76
     this.eventEmitter = new EventEmitter();
70
     this.type = trackMediaType;
78
     this.type = trackMediaType;
71
     this.track = track;
79
     this.track = track;
72
     this.videoType = videoType;
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
         if (RTCBrowserType.isFirefox()) {
106
         if (RTCBrowserType.isFirefox()) {
76
             implementOnEndedHandling(this);
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
  * Returns the type (audio or video) of this track.
131
  * Returns the type (audio or video) of this track.
93
     return this.getType() === MediaType.AUDIO;
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
  * Check if this is videotrack.
155
  * Check if this is videotrack.
98
  */
156
  */
100
     return this.getType() === MediaType.VIDEO;
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
  * Returns the WebRTC MediaStream instance.
171
  * Returns the WebRTC MediaStream instance.
105
  */
172
  */
151
  * @private
218
  * @private
152
  */
219
  */
153
 JitsiTrack.prototype._maybeFireTrackAttached = function (container) {
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
 
247
 
181
     this._maybeFireTrackAttached(container);
248
     this._maybeFireTrackAttached(container);
182
 
249
 
250
+    this._attachTTFMTracker(container);
251
+
183
     return container;
252
     return container;
184
 };
253
 };
185
 
254
 
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
 JitsiTrack.prototype.dispose = function () {
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 Просмотреть файл

1
-/* global __filename, APP, module */
1
+/* global Strophe */
2
+
2
 var logger = require("jitsi-meet-logger").getLogger(__filename);
3
 var logger = require("jitsi-meet-logger").getLogger(__filename);
3
 var EventEmitter = require("events");
4
 var EventEmitter = require("events");
4
-var RTCBrowserType = require("./RTCBrowserType");
5
 var RTCEvents = require("../../service/RTC/RTCEvents.js");
5
 var RTCEvents = require("../../service/RTC/RTCEvents.js");
6
 var RTCUtils = require("./RTCUtils.js");
6
 var RTCUtils = require("./RTCUtils.js");
7
-var JitsiTrack = require("./JitsiTrack");
8
 var JitsiLocalTrack = require("./JitsiLocalTrack.js");
7
 var JitsiLocalTrack = require("./JitsiLocalTrack.js");
8
+import JitsiTrackError from "../../JitsiTrackError";
9
+import * as JitsiTrackErrors from "../../JitsiTrackErrors";
9
 var DataChannels = require("./DataChannels");
10
 var DataChannels = require("./DataChannels");
10
 var JitsiRemoteTrack = require("./JitsiRemoteTrack.js");
11
 var JitsiRemoteTrack = require("./JitsiRemoteTrack.js");
11
 var MediaType = require("../../service/RTC/MediaType");
12
 var MediaType = require("../../service/RTC/MediaType");
17
     var deviceId = null;
18
     var deviceId = null;
18
     tracksInfo.forEach(function(trackInfo){
19
     tracksInfo.forEach(function(trackInfo){
19
         if (trackInfo.mediaType === MediaType.AUDIO) {
20
         if (trackInfo.mediaType === MediaType.AUDIO) {
20
-          deviceId = options.micDeviceId;
21
+            deviceId = options.micDeviceId;
21
         } else if (trackInfo.videoType === VideoType.CAMERA){
22
         } else if (trackInfo.videoType === VideoType.CAMERA){
22
-          deviceId = options.cameraDeviceId;
23
+            deviceId = options.cameraDeviceId;
23
         }
24
         }
24
         var localTrack
25
         var localTrack
25
             = new JitsiLocalTrack(
26
             = new JitsiLocalTrack(
26
                 trackInfo.stream,
27
                 trackInfo.stream,
27
                 trackInfo.track,
28
                 trackInfo.track,
28
                 trackInfo.mediaType,
29
                 trackInfo.mediaType,
29
-                trackInfo.videoType, trackInfo.resolution, deviceId);
30
+                trackInfo.videoType,
31
+                trackInfo.resolution,
32
+                deviceId,
33
+                options.facingMode);
30
         newTracks.push(localTrack);
34
         newTracks.push(localTrack);
31
     });
35
     });
32
     return newTracks;
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
     this.localTracks = [];
41
     this.localTracks = [];
38
     //FIXME: We should support multiple streams per jid.
42
     //FIXME: We should support multiple streams per jid.
39
     this.remoteTracks = {};
43
     this.remoteTracks = {};
42
     this.eventEmitter = new EventEmitter();
46
     this.eventEmitter = new EventEmitter();
43
     var self = this;
47
     var self = this;
44
     this.options = options || {};
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
     // Switch audio output device on all remote audio tracks. Local audio tracks
55
     // Switch audio output device on all remote audio tracks. Local audio tracks
65
     // handle this event by themselves.
56
     // handle this event by themselves.
94
 RTC.obtainAudioAndVideoPermissions = function (options) {
85
 RTC.obtainAudioAndVideoPermissions = function (options) {
95
     return RTCUtils.obtainAudioAndVideoPermissions(options).then(
86
     return RTCUtils.obtainAudioAndVideoPermissions(options).then(
96
         function (tracksInfo) {
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
 RTC.prototype.onIncommingCall = function(event) {
95
 RTC.prototype.onIncommingCall = function(event) {
102
-    if(this.options.config.openSctp)
96
+    if(this.options.config.openSctp) {
103
         this.dataChannels = new DataChannels(event.peerconnection,
97
         this.dataChannels = new DataChannels(event.peerconnection,
104
             this.eventEmitter);
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
 RTC.prototype.selectEndpoint = function (id) {
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
         this.dataChannels.sendSelectedEndpointMessage(id);
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
 RTC.prototype.pinEndpoint = function (id) {
163
 RTC.prototype.pinEndpoint = function (id) {
150
-    if(this.dataChannels)
164
+    if(this.dataChannels) {
151
         this.dataChannels.sendPinnedEndpointMessage(id);
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
 RTC.prototype.addListener = function (type, listener) {
173
 RTC.prototype.addListener = function (type, listener) {
164
 };
183
 };
165
 
184
 
166
 RTC.removeListener = function (eventType, listener) {
185
 RTC.removeListener = function (eventType, listener) {
167
-    RTCUtils.removeListener(eventType, listener)
186
+    RTCUtils.removeListener(eventType, listener);
168
 };
187
 };
169
 
188
 
170
 RTC.isRTCReady = function () {
189
 RTC.isRTCReady = function () {
185
         throw new Error('track must not be null nor undefined');
204
         throw new Error('track must not be null nor undefined');
186
 
205
 
187
     this.localTracks.push(track);
206
     this.localTracks.push(track);
188
-    track._setRTC(this);
207
+
208
+    track.conference = this.conference;
189
 
209
 
190
     if (track.isAudioTrack()) {
210
     if (track.isAudioTrack()) {
191
         this.localAudio = track;
211
         this.localAudio = track;
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
  * @param resource the resource part of the MUC JID
229
  * @param resource the resource part of the MUC JID
209
  * @returns {JitsiRemoteTrack|null}
230
  * @returns {JitsiRemoteTrack|null}
210
  */
231
  */
211
-RTC.prototype.getRemoteAudioTrack = function (resource) {
232
+RTC.prototype.getRemoteTrackByType = function (type, resource) {
212
     if (this.remoteTracks[resource])
233
     if (this.remoteTracks[resource])
213
-        return this.remoteTracks[resource][MediaType.AUDIO];
234
+        return this.remoteTracks[resource][type];
214
     else
235
     else
215
         return null;
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
  * Gets JitsiRemoteTrack for VIDEO MediaType associated with given MUC nickname
250
  * Gets JitsiRemoteTrack for VIDEO MediaType associated with given MUC nickname
220
  * (resource part of the JID).
251
  * (resource part of the JID).
222
  * @returns {JitsiRemoteTrack|null}
253
  * @returns {JitsiRemoteTrack|null}
223
  */
254
  */
224
 RTC.prototype.getRemoteVideoTrack = function (resource) {
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
 RTC.prototype.createRemoteTrack = function (event) {
300
 RTC.prototype.createRemoteTrack = function (event) {
273
     var ownerJid = event.owner;
301
     var ownerJid = event.owner;
274
     var remoteTrack = new JitsiRemoteTrack(
302
     var remoteTrack = new JitsiRemoteTrack(
275
-        this,  ownerJid, event.stream,    event.track,
303
+        this, this.conference, ownerJid, event.stream, event.track,
276
         event.mediaType, event.videoType, event.ssrc, event.muted);
304
         event.mediaType, event.videoType, event.ssrc, event.muted);
277
     var resource = Strophe.getResourceFromJid(ownerJid);
305
     var resource = Strophe.getResourceFromJid(ownerJid);
278
     var remoteTracks
306
     var remoteTracks
287
 
315
 
288
 /**
316
 /**
289
  * Removes all JitsiRemoteTracks associated with given MUC nickname (resource
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
 RTC.prototype.removeRemoteTracks = function (resource) {
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
  * Closes all currently opened data channels.
466
  * Closes all currently opened data channels.
416
  */
467
  */
417
 RTC.prototype.closeAllDataChannels = function () {
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
 RTC.prototype.dispose = function() {
475
 RTC.prototype.dispose = function() {
457
 RTC.prototype.getResourceBySSRC = function (ssrc) {
511
 RTC.prototype.getResourceBySSRC = function (ssrc) {
458
     if((this.localVideo && ssrc == this.localVideo.getSSRC())
512
     if((this.localVideo && ssrc == this.localVideo.getSSRC())
459
         || (this.localAudio && ssrc == this.localAudio.getSSRC())) {
513
         || (this.localAudio && ssrc == this.localAudio.getSSRC())) {
460
-        return Strophe.getResourceFromJid(this.room.myroomjid);
514
+        return this.conference.myUserId();
461
     }
515
     }
462
 
516
 
463
     var track = this.getRemoteTrackBySSRC(ssrc);
517
     var track = this.getRemoteTrackBySSRC(ssrc);
482
     return null;
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
 module.exports = RTC;
580
 module.exports = RTC;

+ 47
- 4
modules/RTC/RTCBrowserType.js Просмотреть файл

30
         return currentBrowser;
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
      * Checks if current browser is Chrome.
46
      * Checks if current browser is Chrome.
35
      * @returns {boolean}
47
      * @returns {boolean}
94
         return RTCBrowserType.isIExplorer() || RTCBrowserType.isSafari();
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
      * Returns Firefox version.
120
      * Returns Firefox version.
99
      * @returns {number|null}
121
      * @returns {number|null}
125
      */
147
      */
126
     isAndroid: function() {
148
     isAndroid: function() {
127
         return isAndroid;
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
     // Add version getters for other browsers when needed
160
     // Add version getters for other browsers when needed
222
     var match
252
     var match
223
         = navigator.userAgent.match(/\b(react[ \t_-]*native)(?:\/(\S+))?/i);
253
         = navigator.userAgent.match(/\b(react[ \t_-]*native)(?:\/(\S+))?/i);
224
     var version;
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
         currentBrowser = RTCBrowserType.RTC_BROWSER_REACT_NATIVE;
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
             version = match[2];
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
     return version;
276
     return version;
234
 }
277
 }

+ 3
- 3
modules/RTC/RTCUIHelper.js Просмотреть файл

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

+ 168
- 56
modules/RTC/RTCUtils.js Просмотреть файл

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
 var logger = require("jitsi-meet-logger").getLogger(__filename);
15
 var logger = require("jitsi-meet-logger").getLogger(__filename);
9
 var RTCBrowserType = require("./RTCBrowserType");
16
 var RTCBrowserType = require("./RTCBrowserType");
10
 var Resolutions = require("../../service/RTC/Resolutions");
17
 var Resolutions = require("../../service/RTC/Resolutions");
11
 var RTCEvents = require("../../service/RTC/RTCEvents");
18
 var RTCEvents = require("../../service/RTC/RTCEvents");
12
-var AdapterJS = require("./adapter.screenshare");
13
 var SDPUtil = require("../xmpp/SDPUtil");
19
 var SDPUtil = require("../xmpp/SDPUtil");
14
 var EventEmitter = require("events");
20
 var EventEmitter = require("events");
15
 var screenObtainer = require("./ScreenObtainer");
21
 var screenObtainer = require("./ScreenObtainer");
16
-var JitsiTrackErrors = require("../../JitsiTrackErrors");
17
-var JitsiTrackError = require("../../JitsiTrackError");
22
+import JitsiTrackError from "../../JitsiTrackError";
18
 var MediaType = require("../../service/RTC/MediaType");
23
 var MediaType = require("../../service/RTC/MediaType");
19
 var VideoType = require("../../service/RTC/VideoType");
24
 var VideoType = require("../../service/RTC/VideoType");
25
+var CameraFacingMode = require("../../service/RTC/CameraFacingMode");
20
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
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
 var eventEmitter = new EventEmitter();
35
 var eventEmitter = new EventEmitter();
23
 
36
 
24
 var AVAILABLE_DEVICES_POLL_INTERVAL_TIME = 3000; // ms
37
 var AVAILABLE_DEVICES_POLL_INTERVAL_TIME = 3000; // ms
31
 // Currently audio output device change is supported only in Chrome and
44
 // Currently audio output device change is supported only in Chrome and
32
 // default output always has 'default' device ID
45
 // default output always has 'default' device ID
33
 var audioOutputDeviceId = 'default'; // default device
46
 var audioOutputDeviceId = 'default'; // default device
47
+// whether user has explicitly set a device to use
48
+var audioOutputChanged = false;
34
 // Disables Acoustic Echo Cancellation
49
 // Disables Acoustic Echo Cancellation
35
 var disableAEC = false;
50
 var disableAEC = false;
36
 // Disables Noise Suppression
51
 // Disables Noise Suppression
42
 
57
 
43
 var currentlyAvailableMediaDevices;
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
         ? function(callback) {
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
         : (MediaStreamTrack && MediaStreamTrack.getSources)
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
 // TODO: currently no browser supports 'devicechange' event even in nightly
88
 // TODO: currently no browser supports 'devicechange' event even in nightly
61
 // builds so no feature/browser detection is used at all. However in future this
89
 // builds so no feature/browser detection is used at all. However in future this
103
  * @param {string} options.desktopStream
131
  * @param {string} options.desktopStream
104
  * @param {string} options.cameraDeviceId
132
  * @param {string} options.cameraDeviceId
105
  * @param {string} options.micDeviceId
133
  * @param {string} options.micDeviceId
106
- * @param {'user'|'environment'} options.facingMode
134
+ * @param {CameraFacingMode} options.facingMode
107
  * @param {bool} firefox_fake_device
135
  * @param {bool} firefox_fake_device
108
  */
136
  */
109
 function getConstraints(um, options) {
137
 function getConstraints(um, options) {
140
             // TODO: Maybe use "exact" syntax if options.facingMode is defined,
168
             // TODO: Maybe use "exact" syntax if options.facingMode is defined,
141
             // but this probably needs to be decided when updating other
169
             // but this probably needs to be decided when updating other
142
             // constraints, as we currently don't use "exact" syntax anywhere.
170
             // constraints, as we currently don't use "exact" syntax anywhere.
171
+            var facingMode = options.facingMode || CameraFacingMode.USER;
172
+
143
             if (isNewStyleConstraintsSupported) {
173
             if (isNewStyleConstraintsSupported) {
144
-                constraints.video.facingMode = options.facingMode || 'user';
174
+                constraints.video.facingMode = facingMode;
145
             }
175
             }
146
-
147
             constraints.video.optional.push({
176
             constraints.video.optional.push({
148
-                facingMode: options.facingMode || 'user'
177
+                facingMode: facingMode
149
             });
178
             });
150
         }
179
         }
151
 
180
 
280
     return constraints;
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
     if (um.indexOf("video") != -1) {
322
     if (um.indexOf("video") != -1) {
285
-        devices.video = available;
323
+        devices.video = videoTracksReceived;
286
     }
324
     }
287
     if (um.indexOf("audio") != -1) {
325
     if (um.indexOf("audio") != -1) {
288
-        devices.audio = available;
326
+        devices.audio = audioTracksReceived;
289
     }
327
     }
290
 
328
 
291
     eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, devices);
329
     eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, devices);
346
  * @param {MediaDeviceInfo[]} devices - list of media devices.
384
  * @param {MediaDeviceInfo[]} devices - list of media devices.
347
  * @emits RTCEvents.DEVICE_LIST_CHANGED
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
     logger.info('list of media devices has changed:', currentlyAvailableMediaDevices);
389
     logger.info('list of media devices has changed:', currentlyAvailableMediaDevices);
352
 
390
 
353
     var videoInputDevices = currentlyAvailableMediaDevices.filter(function (d) {
391
     var videoInputDevices = currentlyAvailableMediaDevices.filter(function (d) {
367
 
405
 
368
     if (videoInputDevices.length &&
406
     if (videoInputDevices.length &&
369
         videoInputDevices.length === videoInputDevicesWithEmptyLabels.length) {
407
         videoInputDevices.length === videoInputDevicesWithEmptyLabels.length) {
370
-        setAvailableDevices(['video'], false);
408
+        devices.video = false;
371
     }
409
     }
372
 
410
 
373
     if (audioInputDevices.length &&
411
     if (audioInputDevices.length &&
374
         audioInputDevices.length === audioInputDevicesWithEmptyLabels.length) {
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
 // In case of IE we continue from 'onReady' callback
419
 // In case of IE we continue from 'onReady' callback
386
     eventEmitter.emit(RTCEvents.RTC_READY, true);
424
     eventEmitter.emit(RTCEvents.RTC_READY, true);
387
     screenObtainer.init(options, GUM);
425
     screenObtainer.init(options, GUM);
388
 
426
 
427
+    // Initialize rawEnumerateDevicesWithCallback
428
+    initRawEnumerateDevicesWithCallback();
429
+
389
     if (RTCUtils.isDeviceListAvailable() && rawEnumerateDevicesWithCallback) {
430
     if (RTCUtils.isDeviceListAvailable() && rawEnumerateDevicesWithCallback) {
390
         rawEnumerateDevicesWithCallback(function (devices) {
431
         rawEnumerateDevicesWithCallback(function (devices) {
391
             currentlyAvailableMediaDevices = devices.splice(0);
432
             currentlyAvailableMediaDevices = devices.splice(0);
557
         if (audioVideo) {
598
         if (audioVideo) {
558
             var audioTracks = audioVideo.getAudioTracks();
599
             var audioTracks = audioVideo.getAudioTracks();
559
             if (audioTracks.length) {
600
             if (audioTracks.length) {
601
+                // eslint-disable-next-line new-cap
560
                 audioStream = new webkitMediaStream();
602
                 audioStream = new webkitMediaStream();
561
                 for (var i = 0; i < audioTracks.length; i++) {
603
                 for (var i = 0; i < audioTracks.length; i++) {
562
                     audioStream.addTrack(audioTracks[i]);
604
                     audioStream.addTrack(audioTracks[i]);
565
 
607
 
566
             var videoTracks = audioVideo.getVideoTracks();
608
             var videoTracks = audioVideo.getVideoTracks();
567
             if (videoTracks.length) {
609
             if (videoTracks.length) {
610
+                // eslint-disable-next-line new-cap
568
                 videoStream = new webkitMediaStream();
611
                 videoStream = new webkitMediaStream();
569
                 for (var j = 0; j < videoTracks.length; j++) {
612
                 for (var j = 0; j < videoTracks.length; j++) {
570
                     videoStream.addTrack(videoTracks[j]);
613
                     videoStream.addTrack(videoTracks[j]);
623
         if (stream
666
         if (stream
624
                 && RTCUtils.isDeviceChangeAvailable('output')
667
                 && RTCUtils.isDeviceChangeAvailable('output')
625
                 && stream.getAudioTracks
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
             element.setSinkId(RTCUtils.getAudioOutputDevice())
672
             element.setSinkId(RTCUtils.getAudioOutputDevice())
628
                 .catch(function (ex) {
673
                 .catch(function (ex) {
629
                     var err = new JitsiTrackError(ex, null, ['audiooutput']);
674
                     var err = new JitsiTrackError(ex, null, ['audiooutput']);
639
         }
684
         }
640
 
685
 
641
         return res;
686
         return res;
642
-    }
687
+    };
643
 }
688
 }
644
 
689
 
645
 /**
690
 /**
746
                     }
791
                     }
747
                     return SDPUtil.filter_special_chars(id);
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
             } else if (RTCBrowserType.isChrome() ||
796
             } else if (RTCBrowserType.isChrome() ||
752
                     RTCBrowserType.isOpera() ||
797
                     RTCBrowserType.isOpera() ||
753
                     RTCBrowserType.isNWJS() ||
798
                     RTCBrowserType.isNWJS() ||
811
                 //AdapterJS.WebRTCPlugin.setLogLevel(
856
                 //AdapterJS.WebRTCPlugin.setLogLevel(
812
                 //    AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
857
                 //    AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
813
                 var self = this;
858
                 var self = this;
814
-                AdapterJS.webRTCReady(function (isPlugin) {
859
+                AdapterJS.webRTCReady(function () {
815
 
860
 
816
                     self.peerconnection = RTCPeerConnection;
861
                     self.peerconnection = RTCPeerConnection;
817
                     self.getUserMedia = window.getUserMedia;
862
                     self.getUserMedia = window.getUserMedia;
844
                         return SDPUtil.filter_special_chars(stream.label);
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
                     resolve();
894
                     resolve();
849
                 });
895
                 });
850
             } else {
896
             } else {
851
                 var errmsg = 'Browser does not appear to be WebRTC-capable';
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
                 reject(new Error(errmsg));
899
                 reject(new Error(errmsg));
857
                 return;
900
                 return;
858
             }
901
             }
859
 
902
 
860
             // Call onReady() if Temasys plugin is not used
903
             // Call onReady() if Temasys plugin is not used
861
             if (!RTCBrowserType.isTemasysPluginUsed()) {
904
             if (!RTCBrowserType.isTemasysPluginUsed()) {
862
-                onReady(options, this.getUserMediaWithConstraints);
905
+                onReady(options, this.getUserMediaWithConstraints.bind(this));
863
                 resolve();
906
                 resolve();
864
             }
907
             }
865
         }.bind(this));
908
         }.bind(this));
878
     **/
921
     **/
879
     getUserMediaWithConstraints: function ( um, success_callback, failure_callback, options) {
922
     getUserMediaWithConstraints: function ( um, success_callback, failure_callback, options) {
880
         options = options || {};
923
         options = options || {};
881
-        var resolution = options.resolution;
882
         var constraints = getConstraints(um, options);
924
         var constraints = getConstraints(um, options);
883
 
925
 
884
         logger.info("Get media constraints", constraints);
926
         logger.info("Get media constraints", constraints);
887
             this.getUserMedia(constraints,
929
             this.getUserMedia(constraints,
888
                 function (stream) {
930
                 function (stream) {
889
                     logger.log('onUserMediaSuccess');
931
                     logger.log('onUserMediaSuccess');
890
-                    setAvailableDevices(um, true);
932
+                    setAvailableDevices(um, stream);
891
                     success_callback(stream);
933
                     success_callback(stream);
892
                 },
934
                 },
893
                 function (error) {
935
                 function (error) {
894
-                    setAvailableDevices(um, false);
936
+                    setAvailableDevices(um, undefined);
895
                     logger.warn('Failed to get access to local media. Error ',
937
                     logger.warn('Failed to get access to local media. Error ',
896
                         error, constraints);
938
                         error, constraints);
897
 
939
 
925
         var self = this;
967
         var self = this;
926
 
968
 
927
         options = options || {};
969
         options = options || {};
970
+        var dsOptions = options.desktopSharingExtensionExternalInstallation;
928
         return new Promise(function (resolve, reject) {
971
         return new Promise(function (resolve, reject) {
929
             var successCallback = function (stream) {
972
             var successCallback = function (stream) {
930
                 resolve(handleLocalStream(stream, options.resolution));
973
                 resolve(handleLocalStream(stream, options.resolution));
955
 
998
 
956
                 if(screenObtainer.isSupported()){
999
                 if(screenObtainer.isSupported()){
957
                     deviceGUM["desktop"] = screenObtainer.obtainStream.bind(
1000
                     deviceGUM["desktop"] = screenObtainer.obtainStream.bind(
958
-                        screenObtainer);
1001
+                        screenObtainer,
1002
+                        dsOptions);
959
                 }
1003
                 }
960
                 // With FF/IE we can't split the stream into audio and video because FF
1004
                 // With FF/IE we can't split the stream into audio and video because FF
961
                 // doesn't support media stream constructors. So, we need to get the
1005
                 // doesn't support media stream constructors. So, we need to get the
1007
                                     devices.push("video");
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
                                 return;
1083
                                 return;
1016
                             }
1084
                             }
1017
                             if(hasDesktop) {
1085
                             if(hasDesktop) {
1018
                                 screenObtainer.obtainStream(
1086
                                 screenObtainer.obtainStream(
1087
+                                    dsOptions,
1019
                                     function (desktopStream) {
1088
                                     function (desktopStream) {
1020
                                         successCallback({audioVideo: stream,
1089
                                         successCallback({audioVideo: stream,
1021
                                             desktopStream: desktopStream});
1090
                                             desktopStream: desktopStream});
1034
                         options);
1103
                         options);
1035
                 } else if (hasDesktop) {
1104
                 } else if (hasDesktop) {
1036
                     screenObtainer.obtainStream(
1105
                     screenObtainer.obtainStream(
1106
+                        dsOptions,
1037
                         function (stream) {
1107
                         function (stream) {
1038
                             successCallback({desktopStream: stream});
1108
                             successCallback({desktopStream: stream});
1039
                         }, function (error) {
1109
                         }, function (error) {
1055
     isRTCReady: function () {
1125
     isRTCReady: function () {
1056
         return rtcReady;
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
         var isEnumerateDevicesAvailable
1131
         var isEnumerateDevicesAvailable
1064
             = navigator.mediaDevices && navigator.mediaDevices.enumerateDevices;
1132
             = navigator.mediaDevices && navigator.mediaDevices.enumerateDevices;
1065
         if (isEnumerateDevicesAvailable) {
1133
         if (isEnumerateDevicesAvailable) {
1066
             return true;
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
      * Returns true if changing the input (camera / microphone) or output
1175
      * Returns true if changing the input (camera / microphone) or output
1101
             mediaStream.stop();
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
         // if we have done createObjectURL, lets clean it
1215
         // if we have done createObjectURL, lets clean it
1105
         var url = mediaStream.jitsiObjectURL;
1216
         var url = mediaStream.jitsiObjectURL;
1106
         if (url) {
1217
         if (url) {
1132
         return featureDetectionAudioEl.setSinkId(deviceId)
1243
         return featureDetectionAudioEl.setSinkId(deviceId)
1133
             .then(function() {
1244
             .then(function() {
1134
                 audioOutputDeviceId = deviceId;
1245
                 audioOutputDeviceId = deviceId;
1246
+                audioOutputChanged = true;
1135
 
1247
 
1136
                 logger.log('Audio output device set to ' + deviceId);
1248
                 logger.log('Audio output device set to ' + deviceId);
1137
 
1249
 

+ 154
- 55
modules/RTC/ScreenObtainer.js Просмотреть файл

1
 /* global chrome, $, alert */
1
 /* global chrome, $, alert */
2
-/* jshint -W003 */
2
+
3
+var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
3
 var logger = require("jitsi-meet-logger").getLogger(__filename);
4
 var logger = require("jitsi-meet-logger").getLogger(__filename);
4
 var RTCBrowserType = require("./RTCBrowserType");
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
  * Indicates whether the Chrome desktop sharing extension is installed.
10
  * Indicates whether the Chrome desktop sharing extension is installed.
36
 
35
 
37
 var GUM = null;
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
  * Handles obtaining a stream from a screen capture on different browsers.
52
  * Handles obtaining a stream from a screen capture on different browsers.
41
  */
53
  */
42
 var ScreenObtainer = {
54
 var ScreenObtainer = {
43
     obtainStream: null,
55
     obtainStream: null,
56
+
44
     /**
57
     /**
45
      * Initializes the function used to obtain a screen capture
58
      * Initializes the function used to obtain a screen capture
46
      * (this.obtainStream).
59
      * (this.obtainStream).
52
      * or disable screen capture (if the value is other).
65
      * or disable screen capture (if the value is other).
53
      * Note that for the "screen" media source to work the
66
      * Note that for the "screen" media source to work the
54
      * 'chrome://flags/#enable-usermedia-screen-capture' flag must be set.
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
         var obtainDesktopStream = null;
72
         var obtainDesktopStream = null;
58
         this.options = options = options || {};
73
         this.options = options = options || {};
59
         GUM = gum;
74
         GUM = gum;
66
             (options.desktopSharingChromeMethod || options.desktopSharing);
81
             (options.desktopSharingChromeMethod || options.desktopSharing);
67
 
82
 
68
         if (RTCBrowserType.isNWJS()) {
83
         if (RTCBrowserType.isNWJS()) {
69
-            obtainDesktopStream = function (onSuccess, onFailure) {
84
+            obtainDesktopStream = (options, onSuccess, onFailure) => {
70
                 window.JitsiMeetNW.obtainDesktopStream (
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
         } else if (RTCBrowserType.isTemasysPluginUsed()) {
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
             if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
121
             if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
78
                 logger.info("Screensharing not supported by this plugin " +
122
                 logger.info("Screensharing not supported by this plugin " +
79
                     "version");
123
                     "version");
109
             } else {
153
             } else {
110
                 obtainDesktopStream = this.obtainScreenOnFirefox;
154
                 obtainDesktopStream = this.obtainScreenOnFirefox;
111
             }
155
             }
112
-
113
         }
156
         }
114
 
157
 
115
         if (!obtainDesktopStream) {
158
         if (!obtainDesktopStream) {
124
      * environment.
167
      * environment.
125
      * @returns {boolean}
168
      * @returns {boolean}
126
      */
169
      */
127
-    isSupported: function() {
170
+    isSupported() {
128
         return !!this.obtainStream;
171
         return !!this.obtainStream;
129
     },
172
     },
173
+
130
     /**
174
     /**
131
      * Obtains a screen capture stream on Firefox.
175
      * Obtains a screen capture stream on Firefox.
132
      * @param callback
176
      * @param callback
133
      * @param errorCallback
177
      * @param errorCallback
134
      */
178
      */
135
-    obtainScreenOnFirefox:
136
-           function (callback, errorCallback) {
137
-        var self = this;
179
+    obtainScreenOnFirefox(options, callback, errorCallback) {
138
         var extensionRequired = false;
180
         var extensionRequired = false;
139
         if (this.options.desktopSharingFirefoxMaxVersionExtRequired === -1 ||
181
         if (this.options.desktopSharingFirefoxMaxVersionExtRequired === -1 ||
140
             (this.options.desktopSharingFirefoxMaxVersionExtRequired >= 0 &&
182
             (this.options.desktopSharingFirefoxMaxVersionExtRequired >= 0 &&
146
         }
188
         }
147
 
189
 
148
         if (!extensionRequired || firefoxExtInstalled === true) {
190
         if (!extensionRequired || firefoxExtInstalled === true) {
149
-            obtainWebRTCScreen(callback, errorCallback);
191
+            obtainWebRTCScreen(options, callback, errorCallback);
150
             return;
192
             return;
151
         }
193
         }
152
 
194
 
159
         // extension if it hasn't.
201
         // extension if it hasn't.
160
         if (firefoxExtInstalled === null) {
202
         if (firefoxExtInstalled === null) {
161
             window.setTimeout(
203
             window.setTimeout(
162
-                function() {
204
+                () => {
163
                     if (firefoxExtInstalled === null)
205
                     if (firefoxExtInstalled === null)
164
                         firefoxExtInstalled = false;
206
                         firefoxExtInstalled = false;
165
-                    self.obtainScreenOnFirefox(callback, errorCallback);
207
+                    this.obtainScreenOnFirefox(callback, errorCallback);
166
                 },
208
                 },
167
-                300
168
-            );
209
+                300);
169
             logger.log("Waiting for detection of jidesha on firefox to " +
210
             logger.log("Waiting for detection of jidesha on firefox to " +
170
                 "finish.");
211
                 "finish.");
171
             return;
212
             return;
182
         errorCallback(
223
         errorCallback(
183
             new JitsiTrackError(JitsiTrackErrors.FIREFOX_EXTENSION_NEEDED));
224
             new JitsiTrackError(JitsiTrackErrors.FIREFOX_EXTENSION_NEEDED));
184
     },
225
     },
226
+
185
     /**
227
     /**
186
      * Asks Chrome extension to call chooseDesktopMedia and gets chrome
228
      * Asks Chrome extension to call chooseDesktopMedia and gets chrome
187
      * 'desktop' stream for returned stream token.
229
      * 'desktop' stream for returned stream token.
188
      */
230
      */
189
-    obtainScreenFromExtension: function (streamCallback, failCallback) {
190
-        var self = this;
231
+    obtainScreenFromExtension(options, streamCallback, failCallback) {
191
         if (chromeExtInstalled) {
232
         if (chromeExtInstalled) {
192
             doGetStreamFromExtension(this.options, streamCallback,
233
             doGetStreamFromExtension(this.options, streamCallback,
193
                 failCallback);
234
                 failCallback);
201
             try {
242
             try {
202
                 chrome.webstore.install(
243
                 chrome.webstore.install(
203
                     getWebStoreInstallUrl(this.options),
244
                     getWebStoreInstallUrl(this.options),
204
-                    function (arg) {
245
+                    arg => {
205
                         logger.log("Extension installed successfully", arg);
246
                         logger.log("Extension installed successfully", arg);
206
                         chromeExtInstalled = true;
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
             } catch(e) {
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
             failCallback(new JitsiTrackError(
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
  * Obtains a desktop stream using getUserMedia.
311
  * Obtains a desktop stream using getUserMedia.
239
  * For this to work on Chrome, the
312
  * For this to work on Chrome, the
243
  * 'media.getusermedia.screensharing.allowed_domains' preference in
316
  * 'media.getusermedia.screensharing.allowed_domains' preference in
244
  * 'about:config'.
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
         //TODO: remove chromeExtensionId (deprecated)
382
         //TODO: remove chromeExtensionId (deprecated)
314
         (options.desktopSharingChromeExtId || options.chromeExtensionId),
383
         (options.desktopSharingChromeExtId || options.chromeExtensionId),
315
         { getVersion: true },
384
         { getVersion: true },
316
-        function (response) {
385
+        response => {
317
             if (!response || !response.version) {
386
             if (!response || !response.version) {
318
                 // Communication failure - assume that no endpoint exists
387
                 // Communication failure - assume that no endpoint exists
319
                 logger.warn(
388
                 logger.warn(
347
             sources: (options.desktopSharingChromeSources ||
416
             sources: (options.desktopSharingChromeSources ||
348
                 options.desktopSharingSources)
417
                 options.desktopSharingSources)
349
         },
418
         },
350
-        function (response) {
419
+        response => {
351
             if (!response) {
420
             if (!response) {
352
                 // possibly re-wraping error message to make code consistent
421
                 // possibly re-wraping error message to make code consistent
353
                 var lastError = chrome.runtime.lastError;
422
                 var lastError = chrome.runtime.lastError;
362
             if (response.streamId) {
431
             if (response.streamId) {
363
                 GUM(
432
                 GUM(
364
                     ['desktop'],
433
                     ['desktop'],
365
-                    function (stream) {
366
-                        streamCallback(stream);
367
-                    },
434
+                    stream => streamCallback(stream),
368
                     failCallback,
435
                     failCallback,
369
-                    {desktopStream: response.streamId});
436
+                    { desktopStream: response.streamId });
370
             } else {
437
             } else {
371
                 // As noted in Chrome Desktop Capture API:
438
                 // As noted in Chrome Desktop Capture API:
372
                 // If user didn't select any source (i.e. canceled the prompt)
439
                 // If user didn't select any source (i.e. canceled the prompt)
405
     // Initialize Chrome extension inline installs
472
     // Initialize Chrome extension inline installs
406
     initInlineInstalls(options);
473
     initInlineInstalls(options);
407
     // Check if extension is installed
474
     // Check if extension is installed
408
-    checkChromeExtInstalled(function (installed, updateRequired) {
475
+    checkChromeExtInstalled((installed, updateRequired) => {
409
         chromeExtInstalled = installed;
476
         chromeExtInstalled = installed;
410
         chromeExtUpdateRequired = updateRequired;
477
         chromeExtUpdateRequired = updateRequired;
411
         logger.info(
478
         logger.info(
414
     }, options);
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
  * Starts the detection of an installed jidesha extension for firefox.
517
  * Starts the detection of an installed jidesha extension for firefox.
419
  * @param options supports "desktopSharingFirefoxDisabled",
518
  * @param options supports "desktopSharingFirefoxDisabled",
431
     }
530
     }
432
 
531
 
433
     var img = document.createElement('img');
532
     var img = document.createElement('img');
434
-    img.onload = function(){
533
+    img.onload = () => {
435
         logger.log("Detected firefox screen sharing extension.");
534
         logger.log("Detected firefox screen sharing extension.");
436
         firefoxExtInstalled = true;
535
         firefoxExtInstalled = true;
437
     };
536
     };
438
-    img.onerror = function(){
537
+    img.onerror = () => {
439
         logger.log("Detected lack of firefox screen sharing extension.");
538
         logger.log("Detected lack of firefox screen sharing extension.");
440
         firefoxExtInstalled = false;
539
         firefoxExtInstalled = false;
441
     };
540
     };

+ 3503
- 401
modules/RTC/adapter.screenshare.js
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 110
- 0
modules/TalkMutedDetection.js Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

47
 
47
 
48
 var callStats = null;
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
 function initCallback (err, msg) {
56
 function initCallback (err, msg) {
51
     logger.log("CallStats Status: err=" + err + " msg=" + msg);
57
     logger.log("CallStats Status: err=" + err + " msg=" + msg);
52
 
58
 
59
+    CallStats.initializeInProgress = false;
60
+
53
     // there is no lib, nothing to report to
61
     // there is no lib, nothing to report to
54
-    if (err !== 'success')
62
+    if (err !== 'success') {
63
+        CallStats.initializeFailed = true;
55
         return;
64
         return;
56
-
57
-    CallStats.initialized = true;
65
+    }
58
 
66
 
59
     var ret = callStats.addNewFabric(this.peerconnection,
67
     var ret = callStats.addNewFabric(this.peerconnection,
60
-        Strophe.getResourceFromJid(this.session.peerjid),
68
+        DEFAULT_REMOTE_USER,
61
         callStats.fabricUsage.multiplex,
69
         callStats.fabricUsage.multiplex,
62
         this.confID,
70
         this.confID,
63
         this.pcCallback.bind(this));
71
         this.pcCallback.bind(this));
64
 
72
 
65
     var fabricInitialized = (ret.status === 'success');
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
     // notify callstats about failures if there were any
85
     // notify callstats about failures if there were any
71
     if (CallStats.reportsQueue.length) {
86
     if (CallStats.reportsQueue.length) {
129
  */
144
  */
130
 var CallStats = _try_catch(function(jingleSession, Settings, options) {
145
 var CallStats = _try_catch(function(jingleSession, Settings, options) {
131
     try{
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
         this.peerconnection = jingleSession.peerconnection.peerconnection;
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
         // The confID is case sensitive!!!
157
         // The confID is case sensitive!!!
145
         this.confID = options.callStatsConfIDNamespace + "/" + options.roomName;
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
         //userID is generated or given by the origin server
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
             this.userID,
167
             this.userID,
151
             initCallback.bind(this));
168
             initCallback.bind(this));
152
 
169
 
153
     } catch (e) {
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
         GlobalOnErrorHandler.callErrorHandler(e);
174
         GlobalOnErrorHandler.callErrorHandler(e);
158
         callStats = null;
175
         callStats = null;
159
         logger.error(e);
176
         logger.error(e);
172
  */
189
  */
173
 CallStats.initialized = false;
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
  * Type of pending reports, can be event or an error.
230
  * Type of pending reports, can be event or an error.
177
  * @type {{ERROR: string, EVENT: string}}
231
  * @type {{ERROR: string, EVENT: string}}
183
 };
237
 };
184
 
238
 
185
 CallStats.prototype.pcCallback = _try_catch(function (err, msg) {
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
     if(!callStats) {
258
     if(!callStats) {
207
         return;
259
         return;
208
     }
260
     }
261
+
209
     // 'focus' is default remote user ID for now
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
     _try_catch(function() {
265
     _try_catch(function() {
216
         logger.debug(
266
         logger.debug(
220
             this.confID,
270
             this.confID,
221
             ssrc,
271
             ssrc,
222
             usageLabel,
272
             usageLabel,
223
-            containerId
224
-        );
273
+            containerId);
225
         if(CallStats.initialized) {
274
         if(CallStats.initialized) {
226
             callStats.associateMstWithUserID(
275
             callStats.associateMstWithUserID(
227
                 this.peerconnection,
276
                 this.peerconnection,
229
                 this.confID,
278
                 this.confID,
230
                 ssrc,
279
                 ssrc,
231
                 usageLabel,
280
                 usageLabel,
232
-                containerId
233
-            );
281
+                containerId);
234
         }
282
         }
235
         else {
283
         else {
236
             CallStats.reportsQueue.push({
284
             CallStats.reportsQueue.push({
237
                 type: reportType.MST_WITH_USERID,
285
                 type: reportType.MST_WITH_USERID,
238
                 data: {
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
     }).bind(this)();
295
     }).bind(this)();
247
 };
296
 };
253
  * @param {CallStats} cs callstats instance related to the event
302
  * @param {CallStats} cs callstats instance related to the event
254
  */
303
  */
255
 CallStats.sendMuteEvent = _try_catch(function (mute, type, cs) {
304
 CallStats.sendMuteEvent = _try_catch(function (mute, type, cs) {
305
+    let event;
256
 
306
 
257
-    var event = null;
258
     if (type === "video") {
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
     CallStats._reportEvent.call(cs, event);
313
     CallStats._reportEvent.call(cs, event);
272
  * @param {CallStats} cs callstats instance related to the event
320
  * @param {CallStats} cs callstats instance related to the event
273
  */
321
  */
274
 CallStats.sendScreenSharingEvent = _try_catch(function (start, cs) {
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
  * @param {CallStats} cs callstats instance related to the event
330
  * @param {CallStats} cs callstats instance related to the event
283
  */
331
  */
284
 CallStats.sendDominantSpeakerEvent = _try_catch(function (cs) {
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
  * @param {CallStats} cs callstats instance related to the event
339
  * @param {CallStats} cs callstats instance related to the event
294
  */
340
  */
295
 CallStats.sendActiveDeviceListEvent = _try_catch(function (devicesData, cs) {
341
 CallStats.sendActiveDeviceListEvent = _try_catch(function (devicesData, cs) {
296
-
297
     CallStats._reportEvent.call(cs, fabricEvent.activeDeviceList, devicesData);
342
     CallStats._reportEvent.call(cs, fabricEvent.activeDeviceList, devicesData);
298
 });
343
 });
299
 
344
 
315
                 type: reportType.EVENT,
360
                 type: reportType.EVENT,
316
                 data: {event: event, eventData: eventData}
361
                 data: {event: event, eventData: eventData}
317
             });
362
             });
363
+        CallStats._checkInitialize();
318
     }
364
     }
319
 };
365
 };
320
 
366
 
329
         callStats.fabricEvent.fabricTerminated, this.confID);
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
  * Notifies CallStats for ice connection failed
379
  * Notifies CallStats for ice connection failed
346
  * @param {RTCPeerConnection} pc connection on which failure occured.
380
  * @param {RTCPeerConnection} pc connection on which failure occured.
360
  */
394
  */
361
 CallStats.prototype.sendFeedback = _try_catch(
395
 CallStats.prototype.sendFeedback = _try_catch(
362
 function(overallFeedback, detailedFeedback) {
396
 function(overallFeedback, detailedFeedback) {
363
-    if(!CallStats.initialized) {
397
+    if(!CallStats.feedbackEnabled) {
364
         return;
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
             type: reportType.ERROR,
425
             type: reportType.ERROR,
393
             data: { type: type, error: e, pc: pc}
426
             data: { type: type, error: e, pc: pc}
394
         });
427
         });
428
+        CallStats._checkInitialize();
395
     }
429
     }
396
     // else just ignore it
430
     // else just ignore it
397
 };
431
 };
468
  * @param {CallStats} cs callstats instance related to the error (optional)
502
  * @param {CallStats} cs callstats instance related to the error (optional)
469
  */
503
  */
470
 CallStats.sendApplicationLog = _try_catch(function (e, cs) {
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
 module.exports = CallStats;
520
 module.exports = CallStats;

+ 0
- 1
modules/statistics/LocalStatsCollector.js Просмотреть файл

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

+ 141
- 165
modules/statistics/RTPStatsCollector.js Просмотреть файл

1
 /* global require */
1
 /* global require */
2
-/* jshint -W101 */
3
 
2
 
3
+var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
4
 var logger = require("jitsi-meet-logger").getLogger(__filename);
4
 var logger = require("jitsi-meet-logger").getLogger(__filename);
5
 var RTCBrowserType = require("../RTC/RTCBrowserType");
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
 /* Whether we support the browser we are running into for logging statistics */
8
 /* Whether we support the browser we are running into for logging statistics */
10
 var browserSupported = RTCBrowserType.isChrome() ||
9
 var browserSupported = RTCBrowserType.isChrome() ||
11
-        RTCBrowserType.isOpera() || RTCBrowserType.isFirefox();
10
+        RTCBrowserType.isOpera() || RTCBrowserType.isFirefox() ||
11
+        RTCBrowserType.isNWJS();
12
 
12
 
13
 /**
13
 /**
14
  * The LibJitsiMeet browser-agnostic names of the browser-specific keys reported
14
  * The LibJitsiMeet browser-agnostic names of the browser-specific keys reported
45
 };
45
 };
46
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_OPERA] =
46
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_OPERA] =
47
     KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
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
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_IEXPLORER] =
50
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_IEXPLORER] =
49
     KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
51
     KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
50
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_SAFARI] =
52
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_SAFARI] =
65
     return Math.round((lostPackets/totalPackets)*100);
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
  * Checks whether a certain record should be included in the logged statistics.
71
  * Checks whether a certain record should be included in the logged statistics.
74
  */
72
  */
103
 }
101
 }
104
 
102
 
105
 /**
103
 /**
106
- * Peer statistics data holder.
104
+ * Holds "statistics" for a single SSRC.
107
  * @constructor
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
         download: 0,
110
         download: 0,
114
         upload: 0
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
  * @param resolution new resolution value to be set.
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
  * Resets the bit rate for given <tt>ssrc</tt> that belong to the peer
143
  * Resets the bit rate for given <tt>ssrc</tt> that belong to the peer
149
  * represented by this instance.
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
 function ConferenceStats() {
151
 function ConferenceStats() {
194
 /**
178
 /**
195
  * <tt>StatsCollector</tt> registers for stats updates of given
179
  * <tt>StatsCollector</tt> registers for stats updates of given
196
  * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
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
  * is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
182
  * is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
199
  * instance as an event source.
183
  * instance as an event source.
200
  *
184
  *
202
  * @param audioLevelsInterval
186
  * @param audioLevelsInterval
203
  * @param statsInterval stats refresh interval given in ms.
187
  * @param statsInterval stats refresh interval given in ms.
204
  * @param eventEmitter
188
  * @param eventEmitter
205
- * @param config {object} supports the following properties: disableAudioLevels,
206
- * disableStats, logStats
207
  * @constructor
189
  * @constructor
208
  */
190
  */
209
 function StatsCollector(
191
 function StatsCollector(
210
         peerconnection,
192
         peerconnection,
211
         audioLevelsInterval,
193
         audioLevelsInterval,
212
         statsInterval,
194
         statsInterval,
213
-        eventEmitter,
214
-        config) {
195
+        eventEmitter) {
215
     // StatsCollector depends entirely on the format of the reports returned by
196
     // StatsCollector depends entirely on the format of the reports returned by
216
     // RTCPeerConnection#getStats. Given that the value of
197
     // RTCPeerConnection#getStats. Given that the value of
217
     // RTCBrowserType#getBrowserType() is very unlikely to change at runtime, it
198
     // RTCBrowserType#getBrowserType() is very unlikely to change at runtime, it
244
     this.baselineAudioLevelsReport = null;
225
     this.baselineAudioLevelsReport = null;
245
     this.currentAudioLevelsReport = null;
226
     this.currentAudioLevelsReport = null;
246
     this.currentStatsReport = null;
227
     this.currentStatsReport = null;
247
-    this.baselineStatsReport = null;
228
+    this.previousStatsReport = null;
248
     this.audioLevelsIntervalId = null;
229
     this.audioLevelsIntervalId = null;
249
     this.eventEmitter = eventEmitter;
230
     this.eventEmitter = eventEmitter;
250
-    this.config = config || {};
251
     this.conferenceStats = new ConferenceStats();
231
     this.conferenceStats = new ConferenceStats();
252
 
232
 
253
     /**
233
     /**
273
 
253
 
274
     this.statsIntervalId = null;
254
     this.statsIntervalId = null;
275
     this.statsIntervalMilis = statsInterval;
255
     this.statsIntervalMilis = statsInterval;
276
-    // Map of ssrcs to PeerStats
256
+    // Map of ssrcs to SsrcStats
277
     this.ssrc2stats = {};
257
     this.ssrc2stats = {};
278
 }
258
 }
279
 
259
 
312
 /**
292
 /**
313
  * Starts stats updates.
293
  * Starts stats updates.
314
  */
294
  */
315
-StatsCollector.prototype.start = function () {
295
+StatsCollector.prototype.start = function (startAudioLevelStats) {
316
     var self = this;
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
         this.statsIntervalId = setInterval(
324
         this.statsIntervalId = setInterval(
343
             function () {
325
             function () {
344
                 // Interval updates
326
                 // Interval updates
363
                             logger.error("Unsupported key:" + e, e);
345
                             logger.error("Unsupported key:" + e, e);
364
                         }
346
                         }
365
 
347
 
366
-                        self.baselineStatsReport = self.currentStatsReport;
348
+                        self.previousStatsReport = self.currentStatsReport;
367
                     },
349
                     },
368
                     self.errorCallback
350
                     self.errorCallback
369
                 );
351
                 );
372
         );
354
         );
373
     }
355
     }
374
 
356
 
375
-    if (this.config.logStats
376
-            && browserSupported
357
+    if (browserSupported
377
             // logging statistics does not support firefox
358
             // logging statistics does not support firefox
378
             && this._browserType !== RTCBrowserType.RTC_BROWSER_FIREFOX) {
359
             && this._browserType !== RTCBrowserType.RTC_BROWSER_FIREFOX) {
379
         this.gatherStatsIntervalId = setInterval(
360
         this.gatherStatsIntervalId = setInterval(
460
     switch (this._browserType) {
441
     switch (this._browserType) {
461
     case RTCBrowserType.RTC_BROWSER_CHROME:
442
     case RTCBrowserType.RTC_BROWSER_CHROME:
462
     case RTCBrowserType.RTC_BROWSER_OPERA:
443
     case RTCBrowserType.RTC_BROWSER_OPERA:
444
+    case RTCBrowserType.RTC_BROWSER_NWJS:
463
         // TODO What about other types of browser which are based on Chrome such
445
         // TODO What about other types of browser which are based on Chrome such
464
         // as NW.js? Every time we want to support a new type browser we have to
446
         // as NW.js? Every time we want to support a new type browser we have to
465
         // go and add more conditions (here and in multiple other places).
447
         // go and add more conditions (here and in multiple other places).
467
         // example, if item has a stat property of type function, then it's very
449
         // example, if item has a stat property of type function, then it's very
468
         // likely that whoever defined it wanted you to call it in order to
450
         // likely that whoever defined it wanted you to call it in order to
469
         // retrieve the value associated with a specific key.
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
         break;
453
         break;
472
     case RTCBrowserType.RTC_BROWSER_REACT_NATIVE:
454
     case RTCBrowserType.RTC_BROWSER_REACT_NATIVE:
473
         // The implementation provided by react-native-webrtc follows the
455
         // The implementation provided by react-native-webrtc follows the
487
         };
469
         };
488
         break;
470
         break;
489
     default:
471
     default:
490
-        itemStatByKey = function (item, key) { return item[key] };
472
+        itemStatByKey = function (item, key) { return item[key]; };
491
     }
473
     }
492
 
474
 
493
     // Compose the 2 functions defined above to get a function which retrieves
475
     // Compose the 2 functions defined above to get a function which retrieves
494
     // the value from a specific report returned by RTCPeerConnection#getStats
476
     // the value from a specific report returned by RTCPeerConnection#getStats
495
     // associated with a specific LibJitsiMeet browser-agnostic name.
477
     // associated with a specific LibJitsiMeet browser-agnostic name.
496
     return function (item, name) {
478
     return function (item, name) {
497
-        return itemStatByKey(item, keyFromName(name))
479
+        return itemStatByKey(item, keyFromName(name));
498
     };
480
     };
499
 };
481
 };
500
 
482
 
502
  * Stats processing logic.
484
  * Stats processing logic.
503
  */
485
  */
504
 StatsCollector.prototype.processStatsReport = function () {
486
 StatsCollector.prototype.processStatsReport = function () {
505
-    if (!this.baselineStatsReport) {
487
+    if (!this.previousStatsReport) {
506
         return;
488
         return;
507
     }
489
     }
508
 
490
 
509
     var getStatValue = this._getStatValue;
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
     for (var idx in this.currentStatsReport) {
506
     for (var idx in this.currentStatsReport) {
512
         var now = this.currentStatsReport[idx];
507
         var now = this.currentStatsReport[idx];
538
             var conferenceStatsTransport = this.conferenceStats.transport;
533
             var conferenceStatsTransport = this.conferenceStats.transport;
539
             if(!conferenceStatsTransport.some(function (t) { return (
534
             if(!conferenceStatsTransport.some(function (t) { return (
540
                         t.ip == ip && t.type == type && t.localip == localip
535
                         t.ip == ip && t.type == type && t.localip == localip
541
-                    )})) {
536
+                    );})) {
542
                 conferenceStatsTransport.push(
537
                 conferenceStatsTransport.push(
543
                     {ip: ip, type: type, localip: localip});
538
                     {ip: ip, type: type, localip: localip});
544
             }
539
             }
563
             continue;
558
             continue;
564
         }
559
         }
565
 
560
 
566
-        var before = this.baselineStatsReport[idx];
561
+        var before = this.previousStatsReport[idx];
567
         var ssrc = getStatValue(now, 'ssrc');
562
         var ssrc = getStatValue(now, 'ssrc');
568
-        if (!before) {
569
-            logger.warn(ssrc + ' not enough data');
563
+        if (!before || !ssrc) {
570
             continue;
564
             continue;
571
         }
565
         }
572
 
566
 
573
-        if(!ssrc)
574
-            continue;
575
-
576
         var ssrcStats
567
         var ssrcStats
577
-          = this.ssrc2stats[ssrc] || (this.ssrc2stats[ssrc] = new PeerStats());
568
+          = this.ssrc2stats[ssrc] || (this.ssrc2stats[ssrc] = new SsrcStats());
578
 
569
 
579
         var isDownloadStream = true;
570
         var isDownloadStream = true;
580
         var key = 'packetsReceived';
571
         var key = 'packetsReceived';
592
         if (!packetsNow || packetsNow < 0)
583
         if (!packetsNow || packetsNow < 0)
593
             packetsNow = 0;
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
         var resolution = {height: null, width: null};
633
         var resolution = {height: null, width: null};
666
         catch(e){/*not supported*/}
647
         catch(e){/*not supported*/}
667
 
648
 
668
         if (resolution.height && resolution.width) {
649
         if (resolution.height && resolution.width) {
669
-            ssrcStats.setSsrcResolution(resolution);
650
+            ssrcStats.setResolution(resolution);
670
         } else {
651
         } else {
671
-            ssrcStats.setSsrcResolution(null);
652
+            ssrcStats.setResolution(null);
672
         }
653
         }
673
     }
654
     }
674
 
655
 
687
     Object.keys(this.ssrc2stats).forEach(
668
     Object.keys(this.ssrc2stats).forEach(
688
         function (ssrc) {
669
         function (ssrc) {
689
             var ssrcStats = this.ssrc2stats[ssrc];
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
             // process bitrate stats
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
             // collect resolutions
683
             // collect resolutions
705
-            resolutions[ssrc] = ssrcStats.ssrc2resolution;
684
+            resolutions[ssrc] = ssrcStats.resolution;
706
         },
685
         },
707
         this
686
         this
708
     );
687
     );
709
 
688
 
689
+    this.eventEmitter.emit(StatisticsEvents.BYTE_SENT_STATS, byteSentStats);
690
+
710
     this.conferenceStats.bitrate
691
     this.conferenceStats.bitrate
711
       = {"upload": bitrateUpload, "download": bitrateDownload};
692
       = {"upload": bitrateUpload, "download": bitrateDownload};
712
 
693
 
720
             calculatePacketLoss(lostPackets.upload, totalPackets.upload)
701
             calculatePacketLoss(lostPackets.upload, totalPackets.upload)
721
     };
702
     };
722
     this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS, {
703
     this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS, {
704
+            "bandwidth": this.conferenceStats.bandwidth,
723
             "bitrate": this.conferenceStats.bitrate,
705
             "bitrate": this.conferenceStats.bitrate,
724
             "packetLoss": this.conferenceStats.packetLoss,
706
             "packetLoss": this.conferenceStats.packetLoss,
725
-            "bandwidth": this.conferenceStats.bandwidth,
726
             "resolution": resolutions,
707
             "resolution": resolutions,
727
             "transport": this.conferenceStats.transport
708
             "transport": this.conferenceStats.transport
728
         });
709
         });
742
     for (var idx in this.currentAudioLevelsReport) {
723
     for (var idx in this.currentAudioLevelsReport) {
743
         var now = this.currentAudioLevelsReport[idx];
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
             continue;
727
             continue;
748
-        }
749
 
728
 
750
         var before = this.baselineAudioLevelsReport[idx];
729
         var before = this.baselineAudioLevelsReport[idx];
751
         var ssrc = getStatValue(now, 'ssrc');
730
         var ssrc = getStatValue(now, 'ssrc');
760
             continue;
739
             continue;
761
         }
740
         }
762
 
741
 
763
-        var ssrcStats
764
-            = this.ssrc2stats[ssrc]
765
-                || (this.ssrc2stats[ssrc] = new PeerStats());
766
-
767
         // Audio level
742
         // Audio level
768
         try {
743
         try {
769
             var audioLevel
744
             var audioLevel
777
         }
752
         }
778
 
753
 
779
         if (audioLevel) {
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
             audioLevel = audioLevel / 32767;
759
             audioLevel = audioLevel / 32767;
783
-            ssrcStats.setSsrcAudioLevel(audioLevel);
784
             this.eventEmitter.emit(
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 Просмотреть файл

1
 /* global require */
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
 var logger = require("jitsi-meet-logger").getLogger(__filename);
6
 var logger = require("jitsi-meet-logger").getLogger(__filename);
7
+var LocalStats = require("./LocalStatsCollector.js");
4
 var RTPStats = require("./RTPStatsCollector.js");
8
 var RTPStats = require("./RTPStatsCollector.js");
5
-var EventEmitter = require("events");
6
-var StatisticsEvents = require("../../service/statistics/Events");
7
-var CallStats = require("./CallStats");
8
 var ScriptUtil = require('../util/ScriptUtil');
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
 // Since callstats.io is a third party, we cannot guarantee the quality of their
17
 // Since callstats.io is a third party, we cannot guarantee the quality of their
12
 // service. More specifically, their server may take noticeably long time to
18
 // service. More specifically, their server may take noticeably long time to
15
 // allow it to prevent people from joining a conference) to (1) start
21
 // allow it to prevent people from joining a conference) to (1) start
16
 // downloading their API as soon as possible and (2) do the downloading
22
 // downloading their API as soon as possible and (2) do the downloading
17
 // asynchronously.
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
     // FIXME At the time of this writing, we hope that the callstats.io API will
33
     // FIXME At the time of this writing, we hope that the callstats.io API will
24
     // have loaded by the time we needed it (i.e. CallStats.init is invoked).
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
  * Log stats via the focus once every this many milliseconds.
62
  * Log stats via the focus once every this many milliseconds.
29
  */
63
  */
58
     return err;
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
 function Statistics(xmpp, options) {
114
 function Statistics(xmpp, options) {
62
     this.rtpStats = null;
115
     this.rtpStats = null;
63
     this.eventEmitter = new EventEmitter();
116
     this.eventEmitter = new EventEmitter();
68
             // Even though AppID and AppSecret may be specified, the integration
121
             // Even though AppID and AppSecret may be specified, the integration
69
             // of callstats.io may be disabled because of globally-disallowed
122
             // of callstats.io may be disabled because of globally-disallowed
70
             // requests to any third parties.
123
             // requests to any third parties.
71
-            && (this.options.disableThirdPartyRequests !== true);
124
+            && (Statistics.disableThirdPartyRequests !== true);
72
     if(this.callStatsIntegrationEnabled)
125
     if(this.callStatsIntegrationEnabled)
73
-        loadCallStatsAPI();
126
+        loadCallStatsAPI(this.options.callStatsCustomScriptUrl);
74
     this.callStats = null;
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
      * Send the stats already saved in rtpStats to be logged via the focus.
133
      * Send the stats already saved in rtpStats to be logged via the focus.
80
 }
136
 }
81
 Statistics.audioLevelsEnabled = false;
137
 Statistics.audioLevelsEnabled = false;
82
 Statistics.audioLevelsInterval = 200;
138
 Statistics.audioLevelsInterval = 200;
139
+Statistics.disableThirdPartyRequests = false;
140
+Statistics.analytics = AnalyticsAdapter;
83
 
141
 
84
 /**
142
 /**
85
  * Array of callstats instances. Used to call Statistics static methods and
143
  * Array of callstats instances. Used to call Statistics static methods and
88
 Statistics.callsStatsInstances = [];
146
 Statistics.callsStatsInstances = [];
89
 
147
 
90
 Statistics.prototype.startRemoteStats = function (peerconnection) {
148
 Statistics.prototype.startRemoteStats = function (peerconnection) {
91
-    if(!Statistics.audioLevelsEnabled)
92
-        return;
93
-
94
     this.stopRemoteStats();
149
     this.stopRemoteStats();
95
 
150
 
96
     try {
151
     try {
97
         this.rtpStats
152
         this.rtpStats
98
             = new RTPStats(peerconnection,
153
             = new RTPStats(peerconnection,
99
                     Statistics.audioLevelsInterval, 2000, this.eventEmitter);
154
                     Statistics.audioLevelsInterval, 2000, this.eventEmitter);
100
-        this.rtpStats.start();
155
+        this.rtpStats.start(Statistics.audioLevelsEnabled);
101
     } catch (e) {
156
     } catch (e) {
102
         this.rtpStats = null;
157
         this.rtpStats = null;
103
         logger.error('Failed to start collecting remote statistics: ' + e);
158
         logger.error('Failed to start collecting remote statistics: ' + e);
139
     this.eventEmitter.on(StatisticsEvents.CONNECTION_STATS, listener);
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
 Statistics.prototype.removeConnectionStatsListener = function (listener) {
197
 Statistics.prototype.removeConnectionStatsListener = function (listener) {
151
     this.eventEmitter.removeListener(StatisticsEvents.CONNECTION_STATS, listener);
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
 Statistics.stopLocalStats = function (stream) {
217
 Statistics.stopLocalStats = function (stream) {
182
 };
227
 };
183
 
228
 
184
 Statistics.prototype.stopRemoteStats = function () {
229
 Statistics.prototype.stopRemoteStats = function () {
185
-    if (!Statistics.audioLevelsEnabled || !this.rtpStats) {
230
+    if (!this.rtpStats) {
186
         return;
231
         return;
187
     }
232
     }
188
 
233
 
204
  * /modules/settings/Settings.js
249
  * /modules/settings/Settings.js
205
  */
250
  */
206
 Statistics.prototype.startCallStats = function (session, settings) {
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
         this.callstats = new CallStats(session, settings, this.options);
255
         this.callstats = new CallStats(session, settings, this.options);
209
         Statistics.callsStatsInstances.push(this.callstats);
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
 };
286
 };
223
 
287
 
224
 /**
288
 /**
225
- * Notifies CallStats for ice connection failed
289
+ * Notifies CallStats and analytics(if present) for ice connection failed
226
  * @param {RTCPeerConnection} pc connection on which failure occured.
290
  * @param {RTCPeerConnection} pc connection on which failure occured.
227
  */
291
  */
228
 Statistics.prototype.sendIceConnectionFailedEvent = function (pc) {
292
 Statistics.prototype.sendIceConnectionFailedEvent = function (pc) {
229
     if(this.callstats)
293
     if(this.callstats)
230
         this.callstats.sendIceConnectionFailedEvent(pc, this.callstats);
294
         this.callstats.sendIceConnectionFailedEvent(pc, this.callstats);
295
+    Statistics.analytics.sendEvent('connection.ice_failed');
231
 };
296
 };
232
 
297
 
233
 /**
298
 /**
372
         CallStats.sendAddIceCandidateFailed(e, pc, this.callstats);
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
  * Adds to CallStats an application log.
441
  * Adds to CallStats an application log.
387
  *
442
  *
406
 Statistics.prototype.sendFeedback = function(overall, detailed) {
461
 Statistics.prototype.sendFeedback = function(overall, detailed) {
407
     if(this.callstats)
462
     if(this.callstats)
408
         this.callstats.sendFeedback(overall, detailed);
463
         this.callstats.sendFeedback(overall, detailed);
464
+    Statistics.analytics.sendEvent("feedback.rating",
465
+        {value: overall, detailed: detailed});
409
 };
466
 };
410
 
467
 
411
 Statistics.LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID;
468
 Statistics.LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID;
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
 module.exports = Statistics;
493
 module.exports = Statistics;

+ 311
- 0
modules/transcription/audioRecorder.js Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

1
+var currentExecutingScript = require("current-executing-script");
2
+
3
+
1
 /**
4
 /**
2
  * Implements utility functions which facilitate the dealing with scripts such
5
  * Implements utility functions which facilitate the dealing with scripts such
3
  * as the download and execution of a JavaScript file.
6
  * as the download and execution of a JavaScript file.
12
      * @param prepend true to schedule the loading of the script as soon as
15
      * @param prepend true to schedule the loading of the script as soon as
13
      * possible or false to schedule the loading of the script at the end of the
16
      * possible or false to schedule the loading of the script at the end of the
14
      * scripts known at the time
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
         var d = document;
26
         var d = document;
18
         var tagName = 'script';
27
         var tagName = 'script';
19
         var script = d.createElement(tagName);
28
         var script = d.createElement(tagName);
20
         var referenceNode = d.getElementsByTagName(tagName)[0];
29
         var referenceNode = d.getElementsByTagName(tagName)[0];
21
 
30
 
22
         script.async = async;
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
         script.src = src;
51
         script.src = src;
24
         if (prepend) {
52
         if (prepend) {
25
             referenceNode.parentNode.insertBefore(script, referenceNode);
53
             referenceNode.parentNode.insertBefore(script, referenceNode);
26
         } else {
54
         } else {
27
             referenceNode.parentNode.appendChild(script);
55
             referenceNode.parentNode.appendChild(script);
28
         }
56
         }
29
-    },
57
+    }
30
 };
58
 };
31
 
59
 
32
 module.exports = ScriptUtil;
60
 module.exports = ScriptUtil;

+ 16
- 14
modules/version/ComponentsVersions.js Просмотреть файл

12
  */
12
  */
13
 ComponentsVersions.VIDEOBRIDGE_COMPONENT = "videobridge";
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
  * @type {string}
16
  * @type {string}
17
  */
17
  */
18
 ComponentsVersions.XMPP_SERVER_COMPONENT = "xmpp";
18
 ComponentsVersions.XMPP_SERVER_COMPONENT = "xmpp";
19
 
19
 
20
 /**
20
 /**
21
  * Creates new instance of <tt>ComponentsVersions</tt> which will be discovering
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
  * @constructor
26
  * @constructor
26
  */
27
  */
27
-function ComponentsVersions(chatRoom) {
28
+function ComponentsVersions(conference) {
28
 
29
 
29
     this.versions = {};
30
     this.versions = {};
30
 
31
 
31
-    this.chatRoom = chatRoom;
32
-    this.chatRoom.addPresenceListener(
32
+    this.conference = conference;
33
+    this.conference.addCommandListener(
33
         'versions', this.processPresence.bind(this));
34
         'versions', this.processPresence.bind(this));
34
 }
35
 }
35
 
36
 
36
 ComponentsVersions.prototype.processPresence =
37
 ComponentsVersions.prototype.processPresence =
37
-function(node, mucResource, mucJid) {
38
+    function(node, mucResource, mucJid) {
38
 
39
 
39
     if (node.attributes.xmlns !== 'http://jitsi.org/jitmeet') {
40
     if (node.attributes.xmlns !== 'http://jitsi.org/jitmeet') {
40
         logger.warn("Ignored presence versions node - invalid xmlns", node);
41
         logger.warn("Ignored presence versions node - invalid xmlns", node);
41
         return;
42
         return;
42
     }
43
     }
43
 
44
 
44
-    if (!this.chatRoom.isFocus(mucJid)) {
45
+    if (!this.conference._isFocus(mucJid)) {
45
         logger.warn(
46
         logger.warn(
46
             "Received versions not from the focus user: " + node, mucJid);
47
             "Received versions not from the focus user: " + node, mucJid);
47
         return;
48
         return;
48
     }
49
     }
49
 
50
 
50
-    var log = "";
51
+    var log = [];
51
     node.children.forEach(function(item){
52
     node.children.forEach(function(item){
52
 
53
 
53
         var componentName = item.attributes.name;
54
         var componentName = item.attributes.name;
65
             this.versions[componentName] = version;
66
             this.versions[componentName] = version;
66
             logger.info("Got " + componentName + " version: " + version);
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
     }.bind(this));
74
     }.bind(this));
72
 
75
 
73
     // logs versions to stats
76
     // logs versions to stats
74
     if (log.length > 0)
77
     if (log.length > 0)
75
-        Statistics.sendLog(log);
78
+        Statistics.sendLog(JSON.stringify(log));
76
 };
79
 };
77
 
80
 
78
 /**
81
 /**
87
 };
90
 };
88
 
91
 
89
 module.exports = ComponentsVersions;
92
 module.exports = ComponentsVersions;
90
-

+ 168
- 67
modules/xmpp/ChatRoom.js Просмотреть файл

1
 /* global Strophe, $, $pres, $iq, $msg */
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
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
5
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
5
 var MediaType = require("../../service/RTC/MediaType");
6
 var MediaType = require("../../service/RTC/MediaType");
6
 var Moderator = require("./moderator");
7
 var Moderator = require("./moderator");
8
 var Recorder = require("./recording");
9
 var Recorder = require("./recording");
9
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
10
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
10
 
11
 
11
-var JIBRI_XMLNS = 'http://jitsi.org/protocol/jibri';
12
-
13
 var parser = {
12
 var parser = {
14
     packet2JSON: function (packet, nodes) {
13
     packet2JSON: function (packet, nodes) {
15
         var self = this;
14
         var self = this;
16
-        $(packet).children().each(function (index) {
15
+        $(packet).children().each(function () {
17
             var tagName = $(this).prop("tagName");
16
             var tagName = $(this).prop("tagName");
18
-            var node = {
17
+            const node = {
19
                 tagName: tagName
18
                 tagName: tagName
20
             };
19
             };
21
             node.attributes = {};
20
             node.attributes = {};
32
         });
31
         });
33
     },
32
     },
34
     JSON2packet: function (nodes, packet) {
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
             if(!node || node === null){
36
             if(!node || node === null){
38
                 continue;
37
                 continue;
39
             }
38
             }
55
  */
54
  */
56
 function filterNodeFromPresenceJSON(pres, nodeName){
55
 function filterNodeFromPresenceJSON(pres, nodeName){
57
     var res = [];
56
     var res = [];
58
-    for(var i = 0; i < pres.length; i++)
57
+    for(let i = 0; i < pres.length; i++)
59
         if(pres[i].tagName === nodeName)
58
         if(pres[i].tagName === nodeName)
60
             res.push(pres[i]);
59
             res.push(pres[i]);
61
 
60
 
74
     this.presMap = {};
73
     this.presMap = {};
75
     this.presHandlers = {};
74
     this.presHandlers = {};
76
     this.joined = false;
75
     this.joined = false;
77
-    this.role = 'none';
76
+    this.role = null;
78
     this.focusMucJid = null;
77
     this.focusMucJid = null;
79
-    this.bridgeIsDown = false;
78
+    this.noBridgeAvailable = false;
80
     this.options = options || {};
79
     this.options = options || {};
81
     this.moderator = new Moderator(this.roomjid, this.xmpp, this.eventEmitter,
80
     this.moderator = new Moderator(this.roomjid, this.xmpp, this.eventEmitter,
82
         settings, {connection: this.xmpp.options, conference: this.options});
81
         settings, {connection: this.xmpp.options, conference: this.options});
83
     this.initPresenceMap();
82
     this.initPresenceMap();
84
     this.session = null;
83
     this.session = null;
85
-    var self = this;
86
     this.lastPresences = {};
84
     this.lastPresences = {};
87
     this.phoneNumber = null;
85
     this.phoneNumber = null;
88
     this.phonePin = null;
86
     this.phonePin = null;
89
     this.connectionTimes = {};
87
     this.connectionTimes = {};
90
     this.participantPropertyListener = null;
88
     this.participantPropertyListener = null;
89
+
90
+    this.locked = false;
91
 }
91
 }
92
 
92
 
93
 ChatRoom.prototype.initPresenceMap = function () {
93
 ChatRoom.prototype.initPresenceMap = function () {
121
 };
121
 };
122
 
122
 
123
 ChatRoom.prototype.join = function (password) {
123
 ChatRoom.prototype.join = function (password) {
124
-    if(password)
125
-        this.password = password;
124
+    this.password = password;
126
     var self = this;
125
     var self = this;
127
     this.moderator.allocateConferenceFocus(function () {
126
     this.moderator.allocateConferenceFocus(function () {
128
         self.sendPresence(true);
127
         self.sendPresence(true);
137
     }
136
     }
138
 
137
 
139
     var pres = $pres({to: to });
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
     // Send XEP-0115 'c' stanza that contains our capabilities info
154
     // Send XEP-0115 'c' stanza that contains our capabilities info
149
     var connection = this.connection;
155
     var connection = this.connection;
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
 ChatRoom.prototype.doLeave = function () {
177
 ChatRoom.prototype.doLeave = function () {
169
     logger.log("do leave", this.myroomjid);
178
     logger.log("do leave", this.myroomjid);
170
     var pres = $pres({to: this.myroomjid, type: 'unavailable' });
179
     var pres = $pres({to: this.myroomjid, type: 'unavailable' });
186
     this.connection.flush();
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
 ChatRoom.prototype.createNonAnonymousRoom = function () {
218
 ChatRoom.prototype.createNonAnonymousRoom = function () {
191
     // http://xmpp.org/extensions/xep-0045.html#createroom-reserved
219
     // http://xmpp.org/extensions/xep-0045.html#createroom-reserved
207
             return;
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
             .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
239
             .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
212
 
240
 
213
         formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
241
         formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
243
     member.jid = jid;
271
     member.jid = jid;
244
     member.isFocus
272
     member.isFocus
245
         = jid && jid.indexOf(this.moderator.getFocusUserJid() + "/") === 0;
273
         = jid && jid.indexOf(this.moderator.getFocusUserJid() + "/") === 0;
246
-
247
     member.isHiddenDomain
274
     member.isHiddenDomain
248
         = jid && jid.indexOf("@") > 0
275
         = jid && jid.indexOf("@") > 0
249
             && this.options.hiddenDomain
276
             && this.options.hiddenDomain
250
-                === jid.substring(jid.indexOf("@") + 1, jid.indexOf("/"))
277
+                === jid.substring(jid.indexOf("@") + 1, jid.indexOf("/"));
251
 
278
 
252
     $(pres).find(">x").remove();
279
     $(pres).find(">x").remove();
253
     var nodes = [];
280
     var nodes = [];
254
     parser.packet2JSON(pres, nodes);
281
     parser.packet2JSON(pres, nodes);
255
     this.lastPresences[from] = nodes;
282
     this.lastPresences[from] = nodes;
256
-    var jibri = null;
283
+    let jibri = null;
257
     // process nodes to extract data needed for MUC_JOINED and MUC_MEMBER_JOINED
284
     // process nodes to extract data needed for MUC_JOINED and MUC_MEMBER_JOINED
258
     // events
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
         switch(node.tagName)
289
         switch(node.tagName)
263
         {
290
         {
264
             case "nick":
291
             case "nick":
271
     }
298
     }
272
 
299
 
273
     if (from == this.myroomjid) {
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
             this.eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED, this.role);
304
             this.eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED, this.role);
277
         }
305
         }
278
         if (!this.joined) {
306
         if (!this.joined) {
280
             var now = this.connectionTimes["muc.joined"] =
308
             var now = this.connectionTimes["muc.joined"] =
281
                 window.performance.now();
309
                 window.performance.now();
282
             logger.log("(TIME) MUC joined:\t", now);
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
             this.eventEmitter.emit(XMPPEvents.MUC_JOINED);
316
             this.eventEmitter.emit(XMPPEvents.MUC_JOINED);
284
         }
317
         }
285
     } else if (this.members[from] === undefined) {
318
     } else if (this.members[from] === undefined) {
287
         this.members[from] = member;
320
         this.members[from] = member;
288
         logger.log('entered', from, member);
321
         logger.log('entered', from, member);
289
         if (member.isFocus) {
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
             this.eventEmitter.emit(
325
             this.eventEmitter.emit(
302
                 XMPPEvents.MUC_MEMBER_JOINED,
326
                 XMPPEvents.MUC_MEMBER_JOINED,
303
                 from, member.nick, member.role, member.isHiddenDomain);
327
                 from, member.nick, member.role, member.isHiddenDomain);
312
                 XMPPEvents.MUC_ROLE_CHANGED, from, member.role);
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
         // store the new display name
352
         // store the new display name
316
         if(member.displayName)
353
         if(member.displayName)
317
             memberOfThis.displayName = member.displayName;
354
             memberOfThis.displayName = member.displayName;
319
 
356
 
320
     // after we had fired member or room joined events, lets fire events
357
     // after we had fired member or room joined events, lets fire events
321
     // for the rest info we got in presence
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
         switch(node.tagName)
362
         switch(node.tagName)
326
         {
363
         {
327
             case "nick":
364
             case "nick":
335
                     }
372
                     }
336
                 }
373
                 }
337
                 break;
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
                     this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
378
                     this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
342
                 }
379
                 }
343
                 break;
380
                 break;
344
             case "jibri-recording-status":
381
             case "jibri-recording-status":
345
-                var jibri = node;
382
+                jibri = node;
346
                 break;
383
                 break;
347
             case "call-control":
384
             case "call-control":
348
                 var att = node.attributes;
385
                 var att = node.attributes;
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
  * Sets the special listener to be used for "command"s whose name starts with
428
  * Sets the special listener to be used for "command"s whose name starts with
375
  * "jitsi_participant_".
429
  * "jitsi_participant_".
442
             reason = reasonSelect.text();
496
             reason = reasonSelect.text();
443
         }
497
         }
444
 
498
 
445
-        this.leave();
499
+        this._dispose();
446
 
500
 
447
         this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
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
         return true;
503
         return true;
450
     }
504
     }
451
 
505
 
452
     // Status code 110 indicates that this notification is "self-presence".
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
         delete this.members[from];
515
         delete this.members[from];
455
         this.onParticipantLeft(from, false);
516
         this.onParticipantLeft(from, false);
456
     }
517
     }
457
     // If the status code is 110 this means we're leaving and we would like
518
     // If the status code is 110 this means we're leaving and we would like
458
     // to remove everyone else from our view, so we trigger the event.
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
             delete this.members[i];
523
             delete this.members[i];
463
             this.onParticipantLeft(i, member.isFocus);
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
         }
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
     if (txt) {
581
     if (txt) {
512
         logger.log('chat', nick, txt);
582
         logger.log('chat', nick, txt);
513
         this.eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
583
         this.eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
527
             // result in reconnection from authorized domain.
597
             // result in reconnection from authorized domain.
528
             // We're either missing Jicofo/Prosody config for anonymous
598
             // We're either missing Jicofo/Prosody config for anonymous
529
             // domains or something is wrong.
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
         } else {
602
         } else {
533
             logger.warn('onPresError ', pres);
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
     } else if($(pres).find('>error>service-unavailable').length) {
606
     } else if($(pres).find('>error>service-unavailable').length) {
537
         logger.warn('Maximum users limit for the room has been reached',
607
         logger.warn('Maximum users limit for the room has been reached',
538
             pres);
608
             pres);
539
-        this.eventEmitter.emit(XMPPEvents.ROOM_MAX_USERS_ERROR, pres);
609
+        this.eventEmitter.emit(XMPPEvents.ROOM_MAX_USERS_ERROR);
540
     } else {
610
     } else {
541
         logger.warn('onPresError ', pres);
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
  * Checks if the user identified by given <tt>mucJid</tt> is the conference
675
  * Checks if the user identified by given <tt>mucJid</tt> is the conference
606
  * focus.
676
  * focus.
607
  * @param mucJid the full MUC address of the user to be checked.
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
 ChatRoom.prototype.isFocus = function (mucJid) {
682
 ChatRoom.prototype.isFocus = function (mucJid) {
611
     var member = this.members[mucJid];
683
     var member = this.members[mucJid];
685
     return this.session.generateNewStreamSSRCInfo();
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
     this.sendVideoInfoPresence(mute);
761
     this.sendVideoInfoPresence(mute);
690
     if(callback)
762
     if(callback)
691
         callback(mute);
763
         callback(mute);
746
             mutedNode = filterNodeFromPresenceJSON(pres, "audiomuted");
818
             mutedNode = filterNodeFromPresenceJSON(pres, "audiomuted");
747
         } else if (mediaType === MediaType.VIDEO) {
819
         } else if (mediaType === MediaType.VIDEO) {
748
             mutedNode = filterNodeFromPresenceJSON(pres, "videomuted");
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
         } else {
826
         } else {
750
             logger.warn("Unsupported media type: " + mediaType);
827
             logger.warn("Unsupported media type: " + mediaType);
751
             data.muted = null;
828
             data.muted = null;
776
  */
853
  */
777
 ChatRoom.prototype.getRecordingState = function () {
854
 ChatRoom.prototype.getRecordingState = function () {
778
     return (this.recording) ? this.recording.getState() : undefined;
855
     return (this.recording) ? this.recording.getState() : undefined;
779
-}
856
+};
780
 
857
 
781
 /**
858
 /**
782
  * Returns the url of the recorded video.
859
  * Returns the url of the recorded video.
783
  */
860
  */
784
 ChatRoom.prototype.getRecordingURL = function () {
861
 ChatRoom.prototype.getRecordingURL = function () {
785
     return (this.recording) ? this.recording.getURL() : null;
862
     return (this.recording) ? this.recording.getURL() : null;
786
-}
863
+};
787
 
864
 
788
 /**
865
 /**
789
  * Starts/stops the recording
866
  * Starts/stops the recording
889
 
966
 
890
 /**
967
 /**
891
  * Leaves the room. Closes the jingle session.
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
 ChatRoom.prototype.leave = function () {
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
     if (this.session) {
998
     if (this.session) {
895
         this.session.close();
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
 module.exports = ChatRoom;
1003
 module.exports = ChatRoom;

+ 11
- 0
modules/xmpp/ConnectionPlugin.js Просмотреть файл

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 Просмотреть файл

3
  * have different implementations depending on the underlying interface used
3
  * have different implementations depending on the underlying interface used
4
  * (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
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
 function JingleSession(me, sid, peerjid, connection,
11
 function JingleSession(me, sid, peerjid, connection,
9
                        media_constraints, ice_config, service, eventEmitter) {
12
                        media_constraints, ice_config, service, eventEmitter) {
58
     // The chat room instance associated with the session.
61
     // The chat room instance associated with the session.
59
     this.room = null;
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
     this.state = null;
68
     this.state = null;
63
 }
69
 }
64
 
70
 
76
         throw new Error(errmsg);
82
         throw new Error(errmsg);
77
     }
83
     }
78
     this.room = room;
84
     this.room = room;
79
-    this.state = 'pending';
85
+    this.state = JingleSessionState.PENDING;
80
     this.initiator = isInitiator ? this.me : this.peerjid;
86
     this.initiator = isInitiator ? this.me : this.peerjid;
81
     this.responder = !isInitiator ? this.me : this.peerjid;
87
     this.responder = !isInitiator ? this.me : this.peerjid;
82
     this.doInitialize();
88
     this.doInitialize();
91
  * Adds the ICE candidates found in the 'contents' array as remote candidates?
97
  * Adds the ICE candidates found in the 'contents' array as remote candidates?
92
  * Note: currently only used on transport-info
98
  * Note: currently only used on transport-info
93
  */
99
  */
100
+// eslint-disable-next-line no-unused-vars
94
 JingleSession.prototype.addIceCandidates = function(contents) {};
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
  *
113
  *
109
  * @param contents an array of Jingle 'content' elements.
114
  * @param contents an array of Jingle 'content' elements.
110
  */
115
  */
116
+// eslint-disable-next-line no-unused-vars
111
 JingleSession.prototype.addSources = function(contents) {};
117
 JingleSession.prototype.addSources = function(contents) {};
112
 
118
 
113
 /**
119
 /**
115
  *
121
  *
116
  * @param contents an array of Jingle 'content' elements.
122
  * @param contents an array of Jingle 'content' elements.
117
  */
123
  */
124
+// eslint-disable-next-line no-unused-vars
118
 JingleSession.prototype.removeSources = function(contents) {};
125
 JingleSession.prototype.removeSources = function(contents) {};
119
 
126
 
120
 /**
127
 /**
121
  * Terminates this Jingle session by sending session-terminate
128
  * Terminates this Jingle session by sending session-terminate
122
  * @param reason XMPP Jingle error condition
129
  * @param reason XMPP Jingle error condition
123
  * @param text some meaningful error message
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
  * Handles an offer from the remote peer (prepares to accept a session).
140
  * Handles an offer from the remote peer (prepares to accept a session).
132
  *        object with details(which is meant more to be printed to the logger
144
  *        object with details(which is meant more to be printed to the logger
133
  *        than analysed in the code, as the error is unrecoverable anyway)
145
  *        than analysed in the code, as the error is unrecoverable anyway)
134
  */
146
  */
147
+// eslint-disable-next-line no-unused-vars
135
 JingleSession.prototype.acceptOffer = function(jingle, success, failure) {};
148
 JingleSession.prototype.acceptOffer = function(jingle, success, failure) {};
136
 
149
 
137
 module.exports = JingleSession;
150
 module.exports = JingleSession;

+ 78
- 29
modules/xmpp/JingleSessionPC.js Просмотреть файл

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
 var JingleSession = require("./JingleSession");
5
 var JingleSession = require("./JingleSession");
5
 var TraceablePeerConnection = require("./TraceablePeerConnection");
6
 var TraceablePeerConnection = require("./TraceablePeerConnection");
6
-var MediaType = require("../../service/RTC/MediaType");
7
 var SDPDiffer = require("./SDPDiffer");
7
 var SDPDiffer = require("./SDPDiffer");
8
 var SDPUtil = require("./SDPUtil");
8
 var SDPUtil = require("./SDPUtil");
9
 var SDP = require("./SDP");
9
 var SDP = require("./SDP");
12
 var RTCBrowserType = require("../RTC/RTCBrowserType");
12
 var RTCBrowserType = require("../RTC/RTCBrowserType");
13
 var RTC = require("../RTC/RTC");
13
 var RTC = require("../RTC/RTC");
14
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
14
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
15
+var Statistics = require("../statistics/statistics");
16
+
17
+import * as JingleSessionState from "./JingleSessionState";
15
 
18
 
16
 /**
19
 /**
17
  * Constant tells how long we're going to wait for IQ response, before timeout
20
  * Constant tells how long we're going to wait for IQ response, before timeout
58
     this.jingleOfferIq = null;
61
     this.jingleOfferIq = null;
59
     this.webrtcIceUdpDisable = !!this.service.options.webrtcIceUdpDisable;
62
     this.webrtcIceUdpDisable = !!this.service.options.webrtcIceUdpDisable;
60
     this.webrtcIceTcpDisable = !!this.service.options.webrtcIceTcpDisable;
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
     this.modifySourcesQueue = async.queue(this._modifySources.bind(this), 1);
71
     this.modifySourcesQueue = async.queue(this._modifySources.bind(this), 1);
63
 }
72
 }
95
             var protocol = candidate.protocol;
104
             var protocol = candidate.protocol;
96
             if (typeof protocol === 'string') {
105
             if (typeof protocol === 'string') {
97
                 protocol = protocol.toLowerCase();
106
                 protocol = protocol.toLowerCase();
98
-                if (protocol == 'tcp') {
107
+                if (protocol === 'tcp' || protocol ==='ssltcp') {
99
                     if (self.webrtcIceTcpDisable)
108
                     if (self.webrtcIceTcpDisable)
100
                         return;
109
                         return;
101
                 } else if (protocol == 'udp') {
110
                 } else if (protocol == 'udp') {
112
     this.peerconnection.onremovestream = function (event) {
121
     this.peerconnection.onremovestream = function (event) {
113
         self.remoteStreamRemoved(event.stream);
122
         self.remoteStreamRemoved(event.stream);
114
     };
123
     };
115
-    this.peerconnection.onsignalingstatechange = function (event) {
124
+    this.peerconnection.onsignalingstatechange = function () {
116
         if (!(self && self.peerconnection)) return;
125
         if (!(self && self.peerconnection)) return;
117
         if (self.peerconnection.signalingState === 'stable') {
126
         if (self.peerconnection.signalingState === 'stable') {
118
             self.wasstable = true;
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
      * RTCPeerConnection.iceConnectionState changes.
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
         if (!(self && self.peerconnection)) return;
137
         if (!(self && self.peerconnection)) return;
130
         var now = window.performance.now();
138
         var now = window.performance.now();
131
         self.room.connectionTimes["ice.state." +
139
         self.room.connectionTimes["ice.state." +
132
             self.peerconnection.iceConnectionState] = now;
140
             self.peerconnection.iceConnectionState] = now;
133
         logger.log("(TIME) ICE " + self.peerconnection.iceConnectionState +
141
         logger.log("(TIME) ICE " + self.peerconnection.iceConnectionState +
134
                     ":\t", now);
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
         switch (self.peerconnection.iceConnectionState) {
148
         switch (self.peerconnection.iceConnectionState) {
136
             case 'connected':
149
             case 'connected':
137
 
150
 
155
                 break;
168
                 break;
156
         }
169
         }
157
     };
170
     };
158
-    this.peerconnection.onnegotiationneeded = function (event) {
171
+    this.peerconnection.onnegotiationneeded = function () {
159
         self.room.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, self);
172
         self.room.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, self);
160
     };
173
     };
161
 };
174
 };
210
                 name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
223
                 name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
211
             }).c('transport', ice);
224
             }).c('transport', ice);
212
             for (var i = 0; i < cands.length; i++) {
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
             // add fingerprint
233
             // add fingerprint
216
             var fingerprint_line = SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session);
234
             var fingerprint_line = SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session);
241
 JingleSessionPC.prototype.readSsrcInfo = function (contents) {
259
 JingleSessionPC.prototype.readSsrcInfo = function (contents) {
242
     var self = this;
260
     var self = this;
243
     $(contents).each(function (idx, content) {
261
     $(contents).each(function (idx, content) {
244
-        var name = $(content).attr('name');
245
-        var mediaType = this.getAttribute('name');
246
         var ssrcs = $(content).find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
262
         var ssrcs = $(content).find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
247
         ssrcs.each(function () {
263
         ssrcs.each(function () {
248
             var ssrc = this.getAttribute('ssrc');
264
             var ssrc = this.getAttribute('ssrc');
269
  */
285
  */
270
 JingleSessionPC.prototype.acceptOffer = function(jingleOffer,
286
 JingleSessionPC.prototype.acceptOffer = function(jingleOffer,
271
                                                  success, failure) {
287
                                                  success, failure) {
272
-    this.state = 'active';
288
+    this.state = JingleSessionState.ACTIVE;
273
     this.setOfferCycle(jingleOffer,
289
     this.setOfferCycle(jingleOffer,
274
         function() {
290
         function() {
275
             // setOfferCycle succeeded, now we have self.localSDP up to date
291
             // setOfferCycle succeeded, now we have self.localSDP up to date
423
     if (this.webrtcIceUdpDisable) {
439
     if (this.webrtcIceUdpDisable) {
424
         localSDP.removeUdpCandidates = true;
440
         localSDP.removeUdpCandidates = true;
425
     }
441
     }
442
+    if (this.failICE) {
443
+        localSDP.failICE = true;
444
+    }
426
     localSDP.toJingle(
445
     localSDP.toJingle(
427
         accept,
446
         accept,
428
         this.initiator == this.me ? 'initiator' : 'responder',
447
         this.initiator == this.me ? 'initiator' : 'responder',
432
     // Calling tree() to print something useful
451
     // Calling tree() to print something useful
433
     accept = accept.tree();
452
     accept = accept.tree();
434
     logger.info("Sending session-accept", accept);
453
     logger.info("Sending session-accept", accept);
435
-
454
+    var self = this;
436
     this.connection.sendIQ(accept,
455
     this.connection.sendIQ(accept,
437
         success,
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
         IQ_TIMEOUT);
462
         IQ_TIMEOUT);
440
     // XXX Videobridge needs WebRTC's answer (ICE ufrag and pwd, DTLS
463
     // XXX Videobridge needs WebRTC's answer (ICE ufrag and pwd, DTLS
441
     // fingerprint and setup) ASAP in order to start the connection
464
     // fingerprint and setup) ASAP in order to start the connection
442
     // establishment.
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
         IQ_TIMEOUT);
546
         IQ_TIMEOUT);
509
 };
547
 };
510
 
548
 
511
-//FIXME: I think this method is not used!
549
+/**
550
+ * @inheritDoc
551
+ */
512
 JingleSessionPC.prototype.terminate = function (reason,  text,
552
 JingleSessionPC.prototype.terminate = function (reason,  text,
513
                                                 success, failure) {
553
                                                 success, failure) {
554
+    this.state = JingleSessionState.ENDED;
555
+
514
     var term = $iq({to: this.peerjid,
556
     var term = $iq({to: this.peerjid,
515
         type: 'set'})
557
         type: 'set'})
516
         .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
558
         .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
556
     // FIXME: dirty waiting
598
     // FIXME: dirty waiting
557
     if (!this.peerconnection.localDescription)
599
     if (!this.peerconnection.localDescription)
558
     {
600
     {
559
-        logger.warn("addSource - localDescription not ready yet")
601
+        logger.warn("addSource - localDescription not ready yet");
560
         setTimeout(function()
602
         setTimeout(function()
561
             {
603
             {
562
                 self.addSource(elem);
604
                 self.addSource(elem);
736
         if (this.webrtcIceUdpDisable) {
778
         if (this.webrtcIceUdpDisable) {
737
             sdp.removeUdpCandidates = true;
779
             sdp.removeUdpCandidates = true;
738
         }
780
         }
781
+        if (this.failICE) {
782
+            sdp.failICE = true;
783
+        }
739
 
784
 
740
         sdp.fromJingle(this.jingleOfferIq);
785
         sdp.fromJingle(this.jingleOfferIq);
741
         this.readSsrcInfo($(this.jingleOfferIq).find(">content"));
786
         this.readSsrcInfo($(this.jingleOfferIq).find(">content"));
765
     this.removessrc = [];
810
     this.removessrc = [];
766
 
811
 
767
     sdp.raw = sdp.session + sdp.media.join('');
812
     sdp.raw = sdp.session + sdp.media.join('');
813
+
768
     /**
814
     /**
769
      * Implements a failure callback which reports an error message and an
815
      * Implements a failure callback which reports an error message and an
770
      * optional error through (1) logger, (2) GlobalOnErrorHandler, and (3)
816
      * optional error through (1) logger, (2) GlobalOnErrorHandler, and (3)
771
      * queueCallback.
817
      * queueCallback.
772
      *
818
      *
773
-     * @param {string} errmsg the error messsage to report
819
+     * @param {string} errmsg the error message to report
774
      * @param {*} error an optional error to report in addition to errmsg
820
      * @param {*} error an optional error to report in addition to errmsg
775
      */
821
      */
776
     function reportError(errmsg, err) {
822
     function reportError(errmsg, err) {
783
         }
829
         }
784
         GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
830
         GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
785
         queueCallback(err);
831
         queueCallback(err);
786
-    };
832
+    }
787
 
833
 
788
     var ufrag = getUfrag(sdp.raw);
834
     var ufrag = getUfrag(sdp.raw);
789
     if (ufrag != self.remoteUfrag) {
835
     if (ufrag != self.remoteUfrag) {
893
             errorCallback(error);
939
             errorCallback(error);
894
         }
940
         }
895
     });
941
     });
896
-}
942
+};
897
 
943
 
898
 /**
944
 /**
899
  * Generate ssrc info object for a stream with the following properties:
945
  * Generate ssrc info object for a stream with the following properties:
997
             errorCallback(error);
1043
             errorCallback(error);
998
         }
1044
         }
999
     });
1045
     });
1000
-}
1046
+};
1001
 
1047
 
1002
 /**
1048
 /**
1003
  * Figures out added/removed ssrcs and send update IQs.
1049
  * Figures out added/removed ssrcs and send update IQs.
1006
  */
1052
  */
1007
 JingleSessionPC.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
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
         return;
1058
         return;
1013
     }
1059
     }
1014
 
1060
 
1103
             error.source = request.tree();
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
         logger.error("Jingle error", error);
1157
         logger.error("Jingle error", error);
1109
         if (failureCb) {
1158
         if (failureCb) {
1459
     var ufragLines = sdp.split('\n').filter(function(line) {
1508
     var ufragLines = sdp.split('\n').filter(function(line) {
1460
         return line.startsWith("a=ice-ufrag:");});
1509
         return line.startsWith("a=ice-ufrag:");});
1461
     if (ufragLines.length > 0) {
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 Просмотреть файл

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 Просмотреть файл

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

+ 3
- 2
modules/xmpp/SDPUtil.js Просмотреть файл

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

+ 52
- 34
modules/xmpp/TraceablePeerConnection.js Просмотреть файл

1
-/* global $ */
1
+/* global mozRTCPeerConnection, webkitRTCPeerConnection */
2
+
3
+import { getLogger } from "jitsi-meet-logger";
4
+const logger = getLogger(__filename);
2
 var RTC = require('../RTC/RTC');
5
 var RTC = require('../RTC/RTC');
3
-var logger = require("jitsi-meet-logger").getLogger(__filename);
4
 var RTCBrowserType = require("../RTC/RTCBrowserType.js");
6
 var RTCBrowserType = require("../RTC/RTCBrowserType.js");
5
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
7
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
6
 var transform = require('sdp-transform');
8
 var transform = require('sdp-transform');
186
             var ssrcOperation = SSRCs[0];
188
             var ssrcOperation = SSRCs[0];
187
             switch(ssrcOperation.type) {
189
             switch(ssrcOperation.type) {
188
                 case "mute":
190
                 case "mute":
189
-                case "addMuted":
191
+                case "addMuted": {
190
                 //FIXME: If we want to support multiple streams we have to add
192
                 //FIXME: If we want to support multiple streams we have to add
191
                 // recv-only ssrcs for the
193
                 // recv-only ssrcs for the
192
                 // muted streams on every change until the stream is unmuted
194
                 // muted streams on every change until the stream is unmuted
194
                 // in the SDP
196
                 // in the SDP
195
                     if(!bLine.ssrcs)
197
                     if(!bLine.ssrcs)
196
                         bLine.ssrcs = [];
198
                         bLine.ssrcs = [];
197
-                    var groups = ssrcOperation.ssrc.groups;
198
-                    var ssrc = null;
199
+                    const groups = ssrcOperation.ssrc.groups;
200
+                    let ssrc = null;
199
                     if(groups && groups.length) {
201
                     if(groups && groups.length) {
200
                         ssrc = groups[0].primarySSRC;
202
                         ssrc = groups[0].primarySSRC;
201
                     } else if(ssrcOperation.ssrc.ssrcs &&
203
                     } else if(ssrcOperation.ssrc.ssrcs &&
218
                     // only 1 video stream that is muted.
220
                     // only 1 video stream that is muted.
219
                     this.recvOnlySSRCs[bLine.type] = ssrc;
221
                     this.recvOnlySSRCs[bLine.type] = ssrc;
220
                     break;
222
                     break;
221
-                case "unmute":
223
+                }
224
+                case "unmute": {
222
                     if(!ssrcOperation.ssrc || !ssrcOperation.ssrc.ssrcs ||
225
                     if(!ssrcOperation.ssrc || !ssrcOperation.ssrc.ssrcs ||
223
                         !ssrcOperation.ssrc.ssrcs.length)
226
                         !ssrcOperation.ssrc.ssrcs.length)
224
                         break;
227
                         break;
225
                     var ssrcMap = {};
228
                     var ssrcMap = {};
226
                     var ssrcLastIdx = ssrcOperation.ssrc.ssrcs.length - 1;
229
                     var ssrcLastIdx = ssrcOperation.ssrc.ssrcs.length - 1;
227
                     for(var i = 0; i < bLine.ssrcs.length; i++) {
230
                     for(var i = 0; i < bLine.ssrcs.length; i++) {
228
-                        var ssrc = bLine.ssrcs[i];
231
+                        const ssrc = bLine.ssrcs[i];
229
                         if (ssrc.attribute !== 'msid' &&
232
                         if (ssrc.attribute !== 'msid' &&
230
                             ssrc.value !== ssrcOperation.msid) {
233
                             ssrc.value !== ssrcOperation.msid) {
231
                             continue;
234
                             continue;
236
                         if(ssrcLastIdx < 0)
239
                         if(ssrcLastIdx < 0)
237
                             break;
240
                             break;
238
                     }
241
                     }
239
-                    var groups = ssrcOperation.ssrc.groups;
242
+                    const groups = ssrcOperation.ssrc.groups;
240
                     if (typeof bLine.ssrcGroups !== 'undefined' &&
243
                     if (typeof bLine.ssrcGroups !== 'undefined' &&
241
                         Array.isArray(bLine.ssrcGroups) && groups &&
244
                         Array.isArray(bLine.ssrcGroups) && groups &&
242
                         groups.length) {
245
                         groups.length) {
277
                     // Storing the unmuted SSRCs.
280
                     // Storing the unmuted SSRCs.
278
                     permSSRCs.push(ssrcOperation);
281
                     permSSRCs.push(ssrcOperation);
279
                     break;
282
                     break;
283
+                }
280
                 default:
284
                 default:
281
-                break;
285
+                    break;
282
             }
286
             }
283
             SSRCs = this.replaceSSRCs[bLine.type].splice(0,1);
287
             SSRCs = this.replaceSSRCs[bLine.type].splice(0,1);
284
         }
288
         }
287
 
291
 
288
         if (!Array.isArray(bLine.ssrcs) || bLine.ssrcs.length === 0)
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
                 = this.recvOnlySSRCs[bLine.type] ||
295
                 = this.recvOnlySSRCs[bLine.type] ||
292
                     RandomUtil.randomInt(1, 0xffffffff);
296
                     RandomUtil.randomInt(1, 0xffffffff);
293
             bLine.ssrcs = [{
297
             bLine.ssrcs = [{
488
         // Removing all cached ssrcs for the streams that are removed or
492
         // Removing all cached ssrcs for the streams that are removed or
489
         // muted.
493
         // muted.
490
         if(ssrcInfo && this.replaceSSRCs[ssrcInfo.mtype]) {
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
                 var op = this.replaceSSRCs[ssrcInfo.mtype][i];
496
                 var op = this.replaceSSRCs[ssrcInfo.mtype][i];
493
                 if(op.type === "unmute" &&
497
                 if(op.type === "unmute" &&
494
                     op.ssrc.ssrcs.join("_") ===
498
                     op.ssrc.ssrcs.join("_") ===
584
     this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
588
     this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
585
     this.peerconnection.createAnswer(
589
     this.peerconnection.createAnswer(
586
         function (answer) {
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
         function(err) {
631
         function(err) {
615
             self.trace('createAnswerOnFailure', err);
632
             self.trace('createAnswerOnFailure', err);
622
 };
639
 };
623
 
640
 
624
 TraceablePeerConnection.prototype.addIceCandidate
641
 TraceablePeerConnection.prototype.addIceCandidate
642
+        // eslint-disable-next-line no-unused-vars
625
         = function (candidate, successCallback, failureCallback) {
643
         = function (candidate, successCallback, failureCallback) {
626
     //var self = this;
644
     //var self = this;
627
     this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
645
     this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));

+ 19
- 7
modules/xmpp/moderator.js Просмотреть файл

174
                 value: true
174
                 value: true
175
             }).up();
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
     if (this.options.conference.audioPacketDelay !== undefined) {
182
     if (this.options.conference.audioPacketDelay !== undefined) {
185
         elem.c(
183
         elem.c(
186
             'property', {
184
             'property', {
236
             name: 'simulcastMode',
234
             name: 'simulcastMode',
237
             value: 'rewriting'
235
             value: 'rewriting'
238
         }).up();
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
     elem.up();
245
     elem.up();
240
     return elem;
246
     return elem;
241
 };
247
 };
369
                 });
375
                 });
370
         return;
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
     var waitMs = self.getNextErrorTimeout();
384
     var waitMs = self.getNextErrorTimeout();
373
     var errmsg = "Focus error, retry after "+ waitMs;
385
     var errmsg = "Focus error, retry after "+ waitMs;
374
     GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
386
     GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));

+ 5
- 4
modules/xmpp/recording.js Просмотреть файл

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
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
5
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
4
 var JitsiRecorderErrors = require("../../JitsiRecorderErrors");
6
 var JitsiRecorderErrors = require("../../JitsiRecorderErrors");
5
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
7
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
6
 
8
 
7
-var logger = require("jitsi-meet-logger").getLogger(__filename);
8
 
9
 
9
 function Recording(type, eventEmitter, connection, focusMucJid, jirecon,
10
 function Recording(type, eventEmitter, connection, focusMucJid, jirecon,
10
     roomjid) {
11
     roomjid) {
115
 };
116
 };
116
 
117
 
117
 Recording.prototype.setRecordingJirecon =
118
 Recording.prototype.setRecordingJirecon =
118
-    function (state, callback, errCallback, options) {
119
+    function (state, callback, errCallback) {
119
 
120
 
120
     if (state == this.state){
121
     if (state == this.state){
121
         errCallback(new Error("Invalid state!"));
122
         errCallback(new Error("Invalid state!"));

+ 114
- 99
modules/xmpp/strophe.emuc.js Просмотреть файл

1
-/* jshint -W117 */
2
 /* a simple MUC connection plugin
1
 /* a simple MUC connection plugin
3
  * can only handle a single MUC room
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
             return true;
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 Просмотреть файл

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
             mandatory: {
19
             mandatory: {
18
                 'OfferToReceiveAudio': true,
20
                 'OfferToReceiveAudio': true,
19
                 'OfferToReceiveVideo': true
21
                 'OfferToReceiveVideo': true
20
             }
22
             }
21
             // MozDontOfferDataChannel: true when this is firefox
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
                 ack.attrs({ type: 'error' });
46
                 ack.attrs({ type: 'error' });
63
                 ack.c('error', {type: 'cancel'})
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
                 this.connection.send(ack);
52
                 this.connection.send(ack);
67
                 return true;
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
             this.connection.send(ack);
73
             this.connection.send(ack);
149
             return true;
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 Просмотреть файл

1
 /* global Strophe */
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 Просмотреть файл

1
 /* global $, $iq, Strophe */
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
  * Ping every 10 sec
9
  * Ping every 10 sec
9
  */
10
  */
10
-var PING_INTERVAL = 10000;
11
+const PING_INTERVAL = 10000;
11
 
12
 
12
 /**
13
 /**
13
  * Ping timeout error after 15 sec of waiting.
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
  * Will close the connection after 3 consecutive ping errors.
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
  * XEP-0199 ping plugin.
27
  * XEP-0199 ping plugin.
24
  *
28
  *
25
  * Registers "urn:xmpp:ping" namespace under Strophe.NS.PING.
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
                 this.failedPings = 0;
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 Просмотреть файл

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 Просмотреть файл

2
 /**
2
 /**
3
  * Strophe logger implementation. Logs from level WARN and above.
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
     Strophe.log = function (level, msg) {
11
     Strophe.log = function (level, msg) {
11
         // Our global handler reports uncaught errors to the stats which may
12
         // Our global handler reports uncaught errors to the stats which may
55
                 return "unknown";
56
                 return "unknown";
56
         }
57
         }
57
     };
58
     };
58
-};
59
+}

+ 343
- 308
modules/xmpp/xmpp.js Просмотреть файл

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
     // Append token as URL param
19
     // Append token as URL param
20
     if (token) {
20
     if (token) {
21
         bosh += (bosh.indexOf('?') == -1 ? '?' : '&') + 'token=' + token;
21
         bosh += (bosh.indexOf('?') == -1 ? '?' : '&') + 'token=' + token;
22
     }
22
     }
23
 
23
 
24
     return new Strophe.Connection(bosh);
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
         // http://xmpp.org/extensions/xep-0167.html#support
65
         // http://xmpp.org/extensions/xep-0167.html#support
67
         // http://xmpp.org/extensions/xep-0176.html#support
66
         // http://xmpp.org/extensions/xep-0176.html#support
68
         disco.addFeature('urn:xmpp:jingle:1');
67
         disco.addFeature('urn:xmpp:jingle:1');
88
         //disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
87
         //disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
89
 
88
 
90
         // Enable Lipsync ?
89
         // Enable Lipsync ?
91
-        if (this.options.enableLipSync && RTCBrowserType.isChrome()) {
90
+        if (RTCBrowserType.isChrome() && false !== this.options.enableLipSync) {
92
             logger.info("Lip-sync enabled !");
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
             this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
178
             this.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED,
154
                 JitsiConnectionErrors.PASSWORD_REQUIRED);
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
         this.connection.flush();
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 Просмотреть файл

4
   "description": "JS library for accessing Jitsi server side deployments",
4
   "description": "JS library for accessing Jitsi server side deployments",
5
   "repository": {
5
   "repository": {
6
     "type": "git",
6
     "type": "git",
7
-    "url": "git://github.com/jitsi/jitsi-meet"
7
+    "url": "git://github.com/jitsi/lib-jitsi-meet"
8
   },
8
   },
9
   "keywords": [
9
   "keywords": [
10
     "jingle",
10
     "jingle",
11
     "webrtc",
11
     "webrtc",
12
     "xmpp",
12
     "xmpp",
13
-    "browser"
13
+    "browser",
14
+    "jitsi"
14
   ],
15
   ],
15
   "author": "",
16
   "author": "",
16
   "readmeFilename": "README.md",
17
   "readmeFilename": "README.md",
17
   "dependencies": {
18
   "dependencies": {
19
+    "async": "0.9.0",
20
+    "current-executing-script": "*",
18
     "events": "*",
21
     "events": "*",
22
+    "jitsi-meet-logger": "jitsi/jitsi-meet-logger",
23
+    "jssha": "1.5.0",
19
     "pako": "*",
24
     "pako": "*",
25
+    "retry": "0.6.1",
20
     "sdp-interop": "0.1.11",
26
     "sdp-interop": "0.1.11",
21
-    "sdp-transform": "1.5.*",
22
     "sdp-simulcast": "0.1.7",
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
     "strophe": "^1.2.2",
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
   "devDependencies": {
33
   "devDependencies": {
33
-    "browserify": "11.1.x",
34
+    "babel-core": "*",
35
+    "babel-loader": "*",
36
+    "babel-polyfill": "*",
37
+    "babel-preset-es2015": "*",
38
+    "eslint": "*",
34
     "jshint": "^2.8.0",
39
     "jshint": "^2.8.0",
35
     "precommit-hook": "^3.0.0",
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
   "scripts": {
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
     "validate": "npm ls"
47
     "validate": "npm ls"
49
   },
48
   },
50
   "pre-commit": [
49
   "pre-commit": [

+ 22
- 0
service/RTC/CameraFacingMode.js Просмотреть файл

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 Просмотреть файл

1
 var RTCEvents = {
1
 var RTCEvents = {
2
     RTC_READY: "rtc.ready",
2
     RTC_READY: "rtc.ready",
3
     DATA_CHANNEL_OPEN: "rtc.data_channel_open",
3
     DATA_CHANNEL_OPEN: "rtc.data_channel_open",
4
+    ENDPOINT_CONN_STATUS_CHANGED: "rtc.endpoint_conn_status_changed",
4
     LASTN_CHANGED: "rtc.lastn_changed",
5
     LASTN_CHANGED: "rtc.lastn_changed",
5
-    DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
6
+    DOMINANT_SPEAKER_CHANGED: "rtc.dominant_speaker_changed",
6
     LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
7
     LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
7
     AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
8
     AVAILABLE_DEVICES_CHANGED: "rtc.available_devices_changed",
8
     TRACK_ATTACHED: "rtc.track_attached",
9
     TRACK_ATTACHED: "rtc.track_attached",
10
+    REMOTE_TRACK_MUTE: "rtc.remote_track_mute",
11
+    REMOTE_TRACK_UNMUTE: "rtc.remote_track_unmute",
9
     AUDIO_OUTPUT_DEVICE_CHANGED: "rtc.audio_output_device_changed",
12
     AUDIO_OUTPUT_DEVICE_CHANGED: "rtc.audio_output_device_changed",
10
     DEVICE_LIST_CHANGED: "rtc.device_list_changed",
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
 module.exports = RTCEvents;
23
 module.exports = RTCEvents;

+ 0
- 7
service/connectionquality/CQEvents.js Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

11
     // Designates an event indicating that an offer (e.g. Jingle
11
     // Designates an event indicating that an offer (e.g. Jingle
12
     // session-initiate) was received.
12
     // session-initiate) was received.
13
     CALL_INCOMING: "xmpp.callincoming.jingle",
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
     CHAT_ERROR_RECEIVED: "xmpp.chat_error_received",
19
     CHAT_ERROR_RECEIVED: "xmpp.chat_error_received",
15
     CONFERENCE_SETUP_FAILED: "xmpp.conference_setup_failed",
20
     CONFERENCE_SETUP_FAILED: "xmpp.conference_setup_failed",
16
     // Designates an event indicating that the connection to the XMPP server
21
     // Designates an event indicating that the connection to the XMPP server
38
     // Designates an event indicating that the display name of a participant
43
     // Designates an event indicating that the display name of a participant
39
     // has changed.
44
     // has changed.
40
     DISPLAY_NAME_CHANGED: "xmpp.display_name_changed",
45
     DISPLAY_NAME_CHANGED: "xmpp.display_name_changed",
41
-    DISPOSE_CONFERENCE: "xmpp.dispose_conference",
42
     ETHERPAD: "xmpp.etherpad",
46
     ETHERPAD: "xmpp.etherpad",
43
     FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
47
     FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
44
     FOCUS_LEFT: "xmpp.focus_left",
48
     FOCUS_LEFT: "xmpp.focus_left",
76
     MUC_MEMBER_JOINED: "xmpp.muc_member_joined",
80
     MUC_MEMBER_JOINED: "xmpp.muc_member_joined",
77
     // Designates an event indicating that a participant left the XMPP MUC.
81
     // Designates an event indicating that a participant left the XMPP MUC.
78
     MUC_MEMBER_LEFT: "xmpp.muc_member_left",
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
     // Designates an event indicating that the MUC role of a participant has
85
     // Designates an event indicating that the MUC role of a participant has
80
     // changed.
86
     // changed.
81
     MUC_ROLE_CHANGED: "xmpp.muc_role_changed",
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
     // Designates an event indicating that a participant in the XMPP MUC has
90
     // Designates an event indicating that a participant in the XMPP MUC has
83
     // advertised that they have audio muted (or unmuted).
91
     // advertised that they have audio muted (or unmuted).
84
     PARTICIPANT_AUDIO_MUTED: "xmpp.audio_muted",
92
     PARTICIPANT_AUDIO_MUTED: "xmpp.audio_muted",
128
     REMOTE_TRACK_REMOVED: "xmpp.remote_track_removed",
136
     REMOTE_TRACK_REMOVED: "xmpp.remote_track_removed",
129
     RESERVATION_ERROR: "xmpp.room_reservation_error",
137
     RESERVATION_ERROR: "xmpp.room_reservation_error",
130
     ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
138
     ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
139
+    ROOM_CONNECT_NOT_ALLOWED_ERROR: 'xmpp.room_connect_error.not_allowed',
131
     ROOM_JOIN_ERROR: 'xmpp.room_join_error',
140
     ROOM_JOIN_ERROR: 'xmpp.room_join_error',
132
     /**
141
     /**
133
      * Indicates that max users limit has been reached.
142
      * Indicates that max users limit has been reached.
139
      * Indicates that the local sendrecv streams in local SDP are changed.
148
      * Indicates that the local sendrecv streams in local SDP are changed.
140
      */
149
      */
141
     SENDRECV_STREAMS_CHANGED: "xmpp.sendrecv_streams_changed",
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
     // TODO: only used in a hack, should probably be removed.
162
     // TODO: only used in a hack, should probably be removed.
143
     SET_LOCAL_DESCRIPTION_ERROR: 'xmpp.set_local_description_error',
163
     SET_LOCAL_DESCRIPTION_ERROR: 'xmpp.set_local_description_error',
144
 
164
 
163
     LOCAL_UFRAG_CHANGED: "xmpp.local_ufrag_changed",
183
     LOCAL_UFRAG_CHANGED: "xmpp.local_ufrag_changed",
164
     // Designates an event indicating that the local ICE username fragment of
184
     // Designates an event indicating that the local ICE username fragment of
165
     // the jingle session has changed.
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
 module.exports = XMPPEvents;
191
 module.exports = XMPPEvents;

+ 64
- 0
webpack.config.js Просмотреть файл

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
+};

Загрузка…
Отмена
Сохранить