@flashphoner/sfusdk 1.0.1-35 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ const constants = SFU.constants;
2
+ const sfu = SFU;
3
+ let mainConfig;
4
+ let localDisplay;
5
+ let remoteDisplay;
6
+ let publishState;
7
+ let playState;
8
+ const PUBLISH = "publish";
9
+ const PLAY = "play";
10
+ const STOP = "stop";
11
+ const PRELOADER_URL="../commons/media/silence.mp3"
12
+
13
+
14
+ /**
15
+ * Default publishing config
16
+ */
17
+ const defaultConfig = {
18
+ room: {
19
+ url: "wss://127.0.0.1:8888",
20
+ name: "ROOM1",
21
+ pin: "1234",
22
+ nickName: "User1"
23
+ },
24
+ media: {
25
+ audio: {
26
+ tracks: [
27
+ {
28
+ source: "mic",
29
+ channels: 1
30
+ }
31
+ ]
32
+ },
33
+ video: {
34
+ tracks: [
35
+ {
36
+ source: "camera",
37
+ width: 640,
38
+ height: 360,
39
+ codec: "H264",
40
+ encodings: [
41
+ { rid: "360p", active: true, maxBitrate: 500000 },
42
+ { rid: "180p", active: true, maxBitrate: 200000, scaleResolutionDownBy: 2 }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Current state object
52
+ */
53
+ const CurrentState = function(prefix) {
54
+ let state = {
55
+ prefix: prefix,
56
+ pc: null,
57
+ session: null,
58
+ room: null,
59
+ set: function(pc, session, room) {
60
+ state.pc = pc;
61
+ state.session = session;
62
+ state.room = room;
63
+ },
64
+ clear: function() {
65
+ state.room = null;
66
+ state.session = null;
67
+ state.pc = null;
68
+ },
69
+ buttonId: function() {
70
+ return state.prefix + "Btn";
71
+ },
72
+ buttonText: function() {
73
+ return (state.prefix.charAt(0).toUpperCase() + state.prefix.slice(1));
74
+ },
75
+ inputId: function() {
76
+ return state.prefix + "Name";
77
+ },
78
+ statusId: function() {
79
+ return state.prefix + "Status";
80
+ },
81
+ formId: function() {
82
+ return state.prefix + "Form";
83
+ },
84
+ errInfoId: function() {
85
+ return state.prefix + "ErrorInfo";
86
+ },
87
+ is: function(value) {
88
+ return (prefix === value);
89
+ }
90
+ };
91
+ return state;
92
+ }
93
+
94
+ /**
95
+ * load config and set default values
96
+ */
97
+ const init = function() {
98
+ let configName = getUrlParam("config") || "./config.json";
99
+ $("#publishBtn").prop('disabled', true);
100
+ $("#playBtn").prop('disabled', true);
101
+ $("#url").prop('disabled', true);
102
+ $("#roomName").prop('disabled', true);
103
+ $("#publishName").prop('disabled', true);
104
+ $("#playName").prop('disabled', true);
105
+ publishState = CurrentState(PUBLISH);
106
+ playState = CurrentState(PLAY);
107
+ $.getJSON(configName, function(cfg){
108
+ mainConfig = cfg;
109
+ onDisconnected(publishState);
110
+ onDisconnected(playState);
111
+ }).fail(function(e){
112
+ //use default config
113
+ console.error("Error reading configuration file " + configName + ": " + e.status + " " + e.statusText)
114
+ console.log("Default config will be used");
115
+ mainConfig = defaultConfig;
116
+ onDisconnected(publishState);
117
+ onDisconnected(playState);
118
+ });
119
+ $("#url").val(setURL());
120
+ $("#roomName").val("ROOM1-"+createUUID(4));
121
+ $("#publishName").val("Publisher1-"+createUUID(4));
122
+ $("#playName").val("Player1-"+createUUID(4));
123
+ }
124
+
125
+ /**
126
+ * connect to server
127
+ */
128
+ const connect = function(state) {
129
+ //create peer connection
130
+ pc = new RTCPeerConnection();
131
+ //get config object for room creation
132
+ const roomConfig = getRoomConfig(mainConfig);
133
+ roomConfig.pc = pc;
134
+ roomConfig.url = $("#url").val();
135
+ roomConfig.roomName = $("#roomName").val();
136
+ roomConfig.nickname = $("#" + state.inputId()).val();
137
+ // clean state display items
138
+ setStatus(state.statusId(), "");
139
+ setStatus(state.errInfoId(), "");
140
+ // connect to server and create a room if not
141
+ const session = sfu.createRoom(roomConfig);
142
+ session.on(constants.SFU_EVENT.CONNECTED, function(room) {
143
+ state.set(pc, session, room);
144
+ onConnected(state);
145
+ setStatus(state.statusId(), "ESTABLISHED", "green");
146
+ }).on(constants.SFU_EVENT.DISCONNECTED, function() {
147
+ state.clear();
148
+ onDisconnected(state);
149
+ setStatus(state.statusId(), "DISCONNECTED", "green");
150
+ }).on(constants.SFU_EVENT.FAILED, function(e) {
151
+ state.clear();
152
+ onDisconnected(state);
153
+ setStatus(state.statusId(), "FAILED", "red");
154
+ setStatus(state.errInfoId(), e.status + " " + e.statusText, "red");
155
+ });
156
+ }
157
+
158
+ const onConnected = function(state) {
159
+ $("#" + state.buttonId()).text("Stop").off('click').click(function () {
160
+ onStopClick(state);
161
+ });
162
+ $('#url').prop('disabled', true);
163
+ $("#roomName").prop('disabled', true);
164
+ $("#" + state.inputId()).prop('disabled', true);
165
+ // Add errors displaying
166
+ state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
167
+ setStatus(state.errInfoId(), e, "red");
168
+ }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
169
+ setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red");
170
+ });
171
+ if (state.is(PUBLISH)) {
172
+ publishStreams(state);
173
+ } else if (state.is(PLAY)) {
174
+ playStreams(state);
175
+ }
176
+ }
177
+
178
+ const onDisconnected = function(state) {
179
+ $("#" + state.buttonId()).text(state.buttonText()).off('click').click(function () {
180
+ onStartClick(state);
181
+ }).prop('disabled', false);
182
+ $("#" + state.inputId()).prop('disabled', false);
183
+ // Check if other session is active
184
+ if ((state.is(PUBLISH) && playState.session)
185
+ || (state.is(PLAY) && publishState.session)) {
186
+ return;
187
+ }
188
+ $('#url').prop('disabled', false);
189
+ $("#roomName").prop('disabled', false);
190
+ }
191
+
192
+ const onStartClick = function(state) {
193
+ if (validateForm("connectionForm") && validateForm(state.formId())) {
194
+ $("#" + state.buttonId()).prop('disabled', true);
195
+ if (state.is(PLAY) && Browser().isSafariWebRTC()) {
196
+ playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () {
197
+ connect(state);
198
+ });
199
+ } else {
200
+ connect(state);
201
+ }
202
+ }
203
+ }
204
+
205
+ const onStopClick = function(state) {
206
+ $("#" + state.buttonId()).prop('disabled', true);
207
+ if (state.is(PUBLISH)) {
208
+ unPublishStreams(state);
209
+ } else if (state.is(PLAY)) {
210
+ stopStreams(state);
211
+ }
212
+ state.session.disconnect();
213
+ }
214
+
215
+ const publishStreams = async function(state) {
216
+ let timerId;
217
+ //create local display item to show local streams
218
+ localDisplay = initLocalDisplay(document.getElementById("localVideo"));
219
+ try {
220
+ //get configured local video streams
221
+ let streams = await getVideoStreams(mainConfig);
222
+ let audioStreams = await getAudioStreams(mainConfig);
223
+ //combine local video streams with audio streams
224
+ streams.push.apply(streams, audioStreams);
225
+ let config = {};
226
+ //add our local streams to the room (to PeerConnection)
227
+ streams.forEach(function (s) {
228
+ //add local stream to local display
229
+ localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream);
230
+ //add each track to PeerConnection
231
+ s.stream.getTracks().forEach((track) => {
232
+ if (s.source === "screen") {
233
+ config[track.id] = s.source;
234
+ }
235
+ addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
236
+ subscribeTrackToEndedEvent(state.room, track, state.pc);
237
+ });
238
+ });
239
+ state.room.join(config);
240
+ // TODO: Use room state or promises to detect if publishing started to enable stop button
241
+ timerId = waitFor(document.getElementById("localVideo"), 3000, state);
242
+ } catch(e) {
243
+ console.error("Failed to capture streams: " + e);
244
+ setStatus(state.errInfoId(), e.name, "red");
245
+ if (timerId) {
246
+ clearTimeout(timerId);
247
+ timerId = null;
248
+ }
249
+ onStopClick(state);
250
+ }
251
+ }
252
+
253
+ // A workaround to check if publishing or playback is started
254
+ const waitFor = function (div, timeout, state) {
255
+ let timerId = setTimeout(function () {
256
+ if (div.innerHTML !== "") {
257
+ // Enable stop button
258
+ $("#" + state.buttonId()).prop('disabled', false);
259
+ }
260
+ else {
261
+ setStatus(state.errInfoId(), "Something went wrong, stopping", "red");
262
+ onStopClick(state);
263
+ }
264
+ }, timeout);
265
+ return timerId;
266
+ }
267
+
268
+ const unPublishStreams = function(state) {
269
+ if (localDisplay) {
270
+ localDisplay.stop();
271
+ }
272
+ }
273
+
274
+ const playStreams = function(state) {
275
+ //create remote display item to show remote streams
276
+ remoteDisplay = initRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc);
277
+ state.room.join();
278
+ $("#" + state.buttonId()).prop('disabled', false);
279
+ }
280
+
281
+ const stopStreams = function(state) {
282
+ if (remoteDisplay) {
283
+ remoteDisplay.stop();
284
+ }
285
+ }
286
+
287
+ const subscribeTrackToEndedEvent = function(room, track, pc) {
288
+ track.addEventListener("ended", function() {
289
+ //track ended, see if we need to cleanup
290
+ let negotiate = false;
291
+ for (const sender of pc.getSenders()) {
292
+ if (sender.track === track) {
293
+ pc.removeTrack(sender);
294
+ //track found, set renegotiation flag
295
+ negotiate = true;
296
+ break;
297
+ }
298
+ }
299
+ if (negotiate) {
300
+ //kickoff renegotiation
301
+ room.updateState();
302
+ }
303
+ });
304
+ };
305
+
306
+ const addTrackToPeerConnection = function(pc, stream, track, encodings) {
307
+ pc.addTransceiver(track, {
308
+ direction: "sendonly",
309
+ streams: [stream],
310
+ sendEncodings: encodings ? encodings : [] //passing encoding types for video simulcast tracks
311
+ });
312
+ }
313
+
314
+ const setStatus = function (status, text, color) {
315
+ const field = document.getElementById(status);
316
+ if (color) {
317
+ field.style.color = color;
318
+ }
319
+ field.innerText = text;
320
+ }
321
+
322
+ const validateForm = function (formId) {
323
+ var valid = true;
324
+ $('#' + formId + ' :text').each(function () {
325
+ if (!$(this).val()) {
326
+ highlightInput($(this));
327
+ valid = false;
328
+ } else {
329
+ removeHighlight($(this));
330
+ }
331
+ });
332
+ return valid;
333
+
334
+ function highlightInput(input) {
335
+ input.closest('.input-group').addClass("has-error");
336
+ }
337
+
338
+ function removeHighlight(input) {
339
+ input.closest('.input-group').removeClass("has-error");
340
+ }
341
+ }
342
+
343
+ const buttonText = function (string) {
344
+ return string.charAt(0).toUpperCase() + string.slice(1);
345
+ }
346
+
@@ -56,6 +56,14 @@ const constants = Object.freeze({
56
56
  */
57
57
  USER_LIST: "USER_LIST",
58
58
 
59
+ //TODO(naz): documentation
60
+ USER_CHATS: "USER_CHATS",
61
+ CHAT_LOADED: "CHAT_LOADED",
62
+ NEW_CHAT: "NEW_CHAT",
63
+ CHAT_DELETED: "CHAT_DELETED",
64
+ CHAT_UPDATED: "CHAT_UPDATED",
65
+ MESSAGE_STATE: "MESSAGE_STATE",
66
+
59
67
  /**
60
68
  * @typedef {Object} UserCalendar
61
69
  * @property events {UserCalendarEvents} object with events
@@ -400,6 +408,11 @@ const constants = Object.freeze({
400
408
  MESSAGE_STATE: "SFU_MESSAGE_STATE",
401
409
  USER_LIST: "SFU_USER_LIST",
402
410
  USER_CALENDAR: "SFU_USER_CALENDAR",
411
+ USER_CHATS: "SFU_USER_CHATS",
412
+ CHAT_LOADED: "SFU_CHAT_LOADED",
413
+ NEW_CHAT: "SFU_NEW_CHAT",
414
+ CHAT_DELETED: "SFU_CHAT_DELETED",
415
+ CHAT_UPDATED: "SFU_UPDATE_CHAT",
403
416
  GET_USER_LIST: "getUserList",
404
417
  GET_USER_CALENDAR: "getUserCalendar",
405
418
  ADD_CALENDAR_EVENT: "addCalendarEvent",
@@ -411,7 +424,14 @@ const constants = Object.freeze({
411
424
  UNSUBSCRIBE_FROM_WAITING_PARTICIPANT: "unsubscribeFromWaitingParticipant",
412
425
  MOVE_TO_WAITING_ROOM: "moveToWaitingRoom",
413
426
  CONFIGURE_WAITING_ROOM: "configureWaitingRoom",
414
- TRACK_CONTENT_HEADER: "a=content:"
427
+ TRACK_CONTENT_HEADER: "a=content:",
428
+ GET_USER_CHATS: "getUserChats",
429
+ LOAD_CHAT: "loadChat",
430
+ CREATE_CHAT: "createChat",
431
+ DELETE_CHAT: "deleteChat",
432
+ RENAME_CHAT: "renameChat",
433
+ ADD_MEMBER_TO_CHAT: "addMemberToChat",
434
+ REMOVE_MEMBER_FROM_CHAT: "removeMemberFromChat"
415
435
  }),
416
436
  /**
417
437
  * @namespace FlashphonerSFUExtended.SFU_OPERATIONS
@@ -430,7 +450,15 @@ const constants = Object.freeze({
430
450
  ASSIGN_ROLE: "ASSIGN_ROLE",
431
451
  SUBSCRIBE_TO_WAITING_PARTICIPANT: "SUBSCRIBE_TO_WAITING_PARTICIPANT",
432
452
  UNSUBSCRIBE_FROM_WAITING_PARTICIPANT: "UNSUBSCRIBE_FROM_WAITING_PARTICIPANT",
433
- MOVE_TO_WAITING_ROOM: "MOVE_TO_WAITING_ROOM"
453
+ MOVE_TO_WAITING_ROOM: "MOVE_TO_WAITING_ROOM",
454
+ CREATE_CHAT: "CREATE_CHAT",
455
+ DELETE_CHAT: "DELETE_CHAT",
456
+ RENAME_CHAT: "RENAME_CHAT",
457
+ LOAD_CHAT: "LOAD_CHAT",
458
+ ADD_MEMBER_TO_CHAT: "ADD_MEMBER_TO_CHAT",
459
+ REMOVE_MEMBER_FROM_CHAT: "REMOVE_MEMBER_FROM_CHAT",
460
+ UPDATE_CHAT_PREFERENCES: "UPDATE_CHAT_PREFERENCES",
461
+ GET_USER_CHATS: "GET_USER_CHATS"
434
462
  }),
435
463
  /**
436
464
  * @namespace FlashphonerSFU.SFU_PARTICIPANT_ROLE
@@ -6,20 +6,31 @@ const create = function(options) {
6
6
 
7
7
  /*
8
8
  * @param {String} msg.to Recipient's id
9
+ * @param {String} msg.chatId Indicates chat this message belongs to
9
10
  * @param {String} msg.body Message body
10
11
  */
11
12
  const sendMessage = function(msg) {
12
13
  return new Promise(function(resolve, reject) {
13
- const message = {
14
- id: uuidv4(),
15
- to: msg.to,
16
- body: msg.body
17
- };
18
- connection.send("sendMessage", message);
19
- pendingMessages[message.id] = {
20
- resolve: resolve,
21
- reject: reject
22
- };
14
+ //validate
15
+ if (!msg) {
16
+ reject(new Error("Can't send null message"));
17
+ } else if (!msg.body || msg.body === "") {
18
+ reject(new Error("Can't send message without a body"));
19
+ } else if (!msg.chatId || msg.chatId === "") {
20
+ reject(new Error("Can't send message without a chatId"));
21
+ } else {
22
+ const message = {
23
+ id: uuidv4(),
24
+ to: msg.to,
25
+ body: msg.body,
26
+ chatId: msg.chatId
27
+ };
28
+ connection.send("sendMessage", message);
29
+ pendingMessages[message.id] = {
30
+ resolve: resolve,
31
+ reject: reject
32
+ };
33
+ }
23
34
  });
24
35
  };
25
36
 
@@ -28,7 +39,7 @@ const create = function(options) {
28
39
  if (promise) {
29
40
  delete pendingMessages[msg[0].status.id];
30
41
  if (msg[0].status.delivered) {
31
- promise.resolve();
42
+ promise.resolve(msg[0].status);
32
43
  } else {
33
44
  promise.reject();
34
45
  }
@@ -8,11 +8,21 @@ function add(id, resolve, reject) {
8
8
  }
9
9
 
10
10
  function resolve(id, args) {
11
- getAndRemovePromise(id).resolve(args);
11
+ const promise = getAndRemovePromise(id);
12
+ if (!promise) {
13
+ return false;
14
+ }
15
+ promise.resolve(args);
16
+ return true;
12
17
  }
13
18
 
14
19
  function reject(id, args) {
15
- getAndRemovePromise(id).reject(args);
20
+ const promise = getAndRemovePromise(id);
21
+ if (!promise) {
22
+ return false;
23
+ }
24
+ promise.reject(args);
25
+ return true;
16
26
  }
17
27
 
18
28
  function promised(id) {
@@ -22,7 +32,7 @@ function promised(id) {
22
32
  function getAndRemovePromise(id) {
23
33
  const promise = promises[id];
24
34
  if (!promise) {
25
- throw new Error("Don't have promise with id " + id)
35
+ return;
26
36
  }
27
37
  delete promises[id];
28
38
  return promise;