Browse Source

Adds simulcast support in meet.

master
George Politis 11 years ago
parent
commit
ffaa9a62b8

+ 60
- 38
app.js View File

@@ -70,18 +70,18 @@ function init() {
70 70
     }
71 71
 
72 72
     obtainAudioAndVideoPermissions(function (stream) {
73
-        var audioStream = new webkitMediaStream(stream);
74
-        var videoStream = new webkitMediaStream(stream);
75
-        var videoTracks = stream.getVideoTracks();
73
+        var audioStream = new webkitMediaStream();
74
+        var videoStream = new webkitMediaStream();
76 75
         var audioTracks = stream.getAudioTracks();
77
-        for (var i = 0; i < videoTracks.length; i++) {
78
-            audioStream.removeTrack(videoTracks[i]);
76
+        var videoTracks = stream.getVideoTracks();
77
+        for (var i = 0; i < audioTracks.length; i++) {
78
+            audioStream.addTrack(audioTracks[i]);
79 79
         }
80 80
         VideoLayout.changeLocalAudio(audioStream);
81 81
         startLocalRtpStatsCollector(audioStream);
82 82
 
83
-        for (i = 0; i < audioTracks.length; i++) {
84
-            videoStream.removeTrack(audioTracks[i]);
83
+        for (i = 0; i < videoTracks.length; i++) {
84
+            videoStream.addTrack(videoTracks[i]);
85 85
         }
86 86
         VideoLayout.changeLocalVideo(videoStream, true);
87 87
         maybeDoJoin();
@@ -237,7 +237,9 @@ function waitForRemoteVideo(selector, ssrc, stream) {
237 237
     if (stream.id === 'mixedmslabel') return;
238 238
 
239 239
     if (selector[0].currentTime > 0) {
240
-        RTC.attachMediaStream(selector, stream); // FIXME: why do i have to do this for FF?
240
+        var simulcast = new Simulcast();
241
+        var videoStream = simulcast.getReceivingVideoStream(stream);
242
+        RTC.attachMediaStream(selector, videoStream); // FIXME: why do i have to do this for FF?
241 243
 
242 244
         // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
243 245
         //        in order to get rid of too many maps
@@ -256,18 +258,40 @@ function waitForRemoteVideo(selector, ssrc, stream) {
256 258
 }
257 259
 
258 260
 $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
261
+    waitForPresence(data, sid);
262
+});
263
+
264
+function waitForPresence(data, sid) {
259 265
     var sess = connection.jingle.sessions[sid];
260 266
 
261 267
     var thessrc;
262 268
     // look up an associated JID for a stream id
263 269
     if (data.stream.id.indexOf('mixedmslabel') === -1) {
270
+        // look only at a=ssrc: and _not_ at a=ssrc-group: lines
264 271
         var ssrclines
265
-            = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
272
+            = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc:');
266 273
         ssrclines = ssrclines.filter(function (line) {
267 274
             return line.indexOf('mslabel:' + data.stream.label) !== -1;
268 275
         });
269 276
         if (ssrclines.length) {
270 277
             thessrc = ssrclines[0].substring(7).split(' ')[0];
278
+
279
+            // We signal our streams (through Jingle to the focus) before we set
280
+            // our presence (through which peers associate remote streams to
281
+            // jids). So, it might arrive that a remote stream is added but
282
+            // ssrc2jid is not yet updated and thus data.peerjid cannot be
283
+            // successfully set. Here we wait for up to a second for the
284
+            // presence to arrive.
285
+
286
+            if (!ssrc2jid[thessrc]) {
287
+                setTimeout(function(d, s) {
288
+                    return function() {
289
+                            waitForPresence(d, s);
290
+                    }
291
+                }(data, sid), 250);
292
+                return;
293
+            }
294
+
271 295
             // ok to overwrite the one from focus? might save work in colibri.js
272 296
             console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
273 297
             if (ssrc2jid[thessrc]) {
@@ -276,6 +300,9 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
276 300
         }
277 301
     }
278 302
 
303
+    // NOTE(gp) now that we have simulcast, a media stream can have more than 1
304
+    // ssrc. We should probably take that into account in our MediaStream
305
+    // wrapper.
279 306
     mediaStreams.push(new MediaStream(data, sid, thessrc));
280 307
 
281 308
     var container;
@@ -322,7 +349,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
322 349
             sendKeyframe(sess.peerconnection);
323 350
         }, 3000);
324 351
     }
325
-});
352
+}
326 353
 
327 354
 /**
328 355
  * Returns the JID of the user to whom given <tt>videoSrc</tt> belongs.
@@ -532,40 +559,35 @@ $(document).bind('callterminated.jingle', function (event, sid, jid, reason) {
532 559
 $(document).bind('setLocalDescription.jingle', function (event, sid) {
533 560
     // put our ssrcs into presence so other clients can identify our stream
534 561
     var sess = connection.jingle.sessions[sid];
535
-    var newssrcs = {};
536
-    var directions = {};
537
-    var localSDP = new SDP(sess.peerconnection.localDescription.sdp);
538
-    localSDP.media.forEach(function (media) {
539
-        var type = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
540
-
541
-        if (SDPUtil.find_line(media, 'a=ssrc:')) {
542
-            // assumes a single local ssrc
543
-            var ssrc = SDPUtil.find_line(media, 'a=ssrc:').substring(7).split(' ')[0];
544
-            newssrcs[type] = ssrc;
545
-
546
-            directions[type] = (
547
-                SDPUtil.find_line(media, 'a=sendrecv') ||
548
-                SDPUtil.find_line(media, 'a=recvonly') ||
549
-                SDPUtil.find_line(media, 'a=sendonly') ||
550
-                SDPUtil.find_line(media, 'a=inactive') ||
551
-                'a=sendrecv').substr(2);
552
-        }
562
+    var newssrcs = [];
563
+    var simulcast = new Simulcast();
564
+    var media = simulcast.parseMedia(sess.peerconnection.localDescription);
565
+    media.forEach(function (media) {
566
+
567
+        // TODO(gp) maybe exclude FID streams?
568
+        Object.keys(media.sources).forEach(function(ssrc) {
569
+            newssrcs.push({
570
+                'ssrc': ssrc,
571
+                'type': media.type,
572
+                'direction': media.direction
573
+            });
574
+        });
553 575
     });
554 576
     console.log('new ssrcs', newssrcs);
555 577
 
556 578
     // Have to clear presence map to get rid of removed streams
557 579
     connection.emuc.clearPresenceMedia();
558
-    var i = 0;
559
-    Object.keys(newssrcs).forEach(function (mtype) {
560
-        i++;
561
-        var type = mtype;
562
-        // Change video type to screen
563
-        if (mtype === 'video' && isUsingScreenStream) {
564
-            type = 'screen';
580
+
581
+    if (newssrcs.length > 0) {
582
+        for (var i = 1; i <= newssrcs.length; i ++) {
583
+            // Change video type to screen
584
+            if (newssrcs[i-1].type === 'video' && isUsingScreenStream) {
585
+                newssrcs[i-1].type = 'screen';
586
+            }
587
+            connection.emuc.addMediaToPresence(i,
588
+                newssrcs[i-1].type, newssrcs[i-1].ssrc, newssrcs[i-1].direction);
565 589
         }
566
-        connection.emuc.addMediaToPresence(i, type, newssrcs[mtype], directions[mtype]);
567
-    });
568
-    if (i > 0) {
590
+
569 591
         connection.emuc.sendPresence();
570 592
     }
571 593
 });

+ 2
- 0
config.js View File

@@ -22,5 +22,7 @@ var config = {
22 22
     useBundle: true,
23 23
     enableRecording: false,
24 24
     enableWelcomePage: false,
25
+    enableSimulcast: false,
26
+    useNativeSimulcast: false,
25 27
     isBrand: false
26 28
 };

+ 5
- 0
data_channels.js View File

@@ -84,6 +84,11 @@ function onDataChannel(event)
84 84
                         'lastnchanged',
85 85
                         [lastNEndpoints, endpointsEnteringLastN, stream]);
86 86
             }
87
+            else if ("SimulcastLayersChangedEvent" === colibriClass)
88
+            {
89
+                var endpointSimulcastLayers = obj.endpointSimulcastLayers;
90
+                $(document).trigger('simulcastlayerschanged', [endpointSimulcastLayers]);
91
+            }
87 92
             else
88 93
             {
89 94
                 console.debug("Data channel JSON-formatted message: ", obj);

+ 1
- 0
index.html View File

@@ -10,6 +10,7 @@
10 10
     <meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
11 11
     <meta itemprop="image" content="/images/jitsilogo.png"/>
12 12
     <script src="libs/jquery-2.1.1.min.js"></script>
13
+    <script src="simulcast.js?v=1"></script><!-- simulcast handling -->
13 14
     <script src="libs/strophe/strophe.jingle.adapter.js?v=1"></script><!-- strophe.jingle bundles -->
14 15
     <script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script>
15 16
     <script src="libs/strophe/strophe.jingle.js?v=1"></script>

+ 117
- 7
libs/colibri/colibri.focus.js View File

@@ -42,6 +42,7 @@ function ColibriFocus(connection, bridgejid) {
42 42
 
43 43
     this.bridgejid = bridgejid;
44 44
     this.peers = [];
45
+    this.remoteStreams = [];
45 46
     this.confid = null;
46 47
 
47 48
     /**
@@ -142,6 +143,7 @@ ColibriFocus.prototype.makeConference = function (peers) {
142 143
                 event.peerjid = jid;
143 144
             }
144 145
         });
146
+        self.remoteStreams.push(event.stream);
145 147
         $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
146 148
     };
147 149
     this.peerconnection.onicecandidate = function (event) {
@@ -525,9 +527,11 @@ ColibriFocus.prototype.createdConference = function (result) {
525 527
         }
526 528
     }
527 529
     bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
530
+    var bridgeDesc = new RTCSessionDescription({type: 'offer', sdp: bridgeSDP.raw});
531
+    var simulcast = new Simulcast();
532
+    var bridgeDesc = simulcast.transformBridgeDescription(bridgeDesc);
528 533
 
529
-    this.peerconnection.setRemoteDescription(
530
-        new RTCSessionDescription({type: 'offer', sdp: bridgeSDP.raw}),
534
+    this.peerconnection.setRemoteDescription(bridgeDesc,
531 535
         function () {
532 536
             console.log('setRemoteDescription success');
533 537
             self.peerconnection.createAnswer(
@@ -553,6 +557,24 @@ ColibriFocus.prototype.createdConference = function (result) {
553 557
                                         endpoint: self.myMucResource
554 558
                                     });
555 559
 
560
+                                    // signal (through COLIBRI) to the bridge
561
+                                    // the SSRC groups of the participant
562
+                                    // that plays the role of the focus
563
+                                    var ssrc_group_lines = SDPUtil.find_lines(media, 'a=ssrc-group:');
564
+                                    var idx = 0;
565
+                                    ssrc_group_lines.forEach(function(line) {
566
+                                        idx = line.indexOf(' ');
567
+                                        var semantics = line.substr(0, idx).substr(13);
568
+                                        var ssrcs = line.substr(14 + semantics.length).split(' ');
569
+                                        if (ssrcs.length != 0) {
570
+                                            elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
571
+                                            ssrcs.forEach(function(ssrc) {
572
+                                                elem.c('source', { ssrc: ssrc })
573
+                                                    .up();
574
+                                            });
575
+                                            elem.up();
576
+                                        }
577
+                                    });
556 578
                                     // FIXME: should reuse code from .toJingle
557 579
                                     for (var j = 0; j < mline.fmt.length; j++)
558 580
                                     {
@@ -646,6 +668,7 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
646 668
                 sdp.removeMediaLines(i, 'a=rtcp-mux');
647 669
             }
648 670
             sdp.removeMediaLines(i, 'a=ssrc:');
671
+            sdp.removeMediaLines(i, 'a=ssrc-group:');
649 672
             sdp.removeMediaLines(i, 'a=crypto:');
650 673
             sdp.removeMediaLines(i, 'a=candidate:');
651 674
             sdp.removeMediaLines(i, 'a=ice-options:google-ice');
@@ -655,14 +678,20 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
655 678
             sdp.removeMediaLines(i, 'a=setup:');
656 679
 
657 680
             if (1) { //i > 0) { // not for audio FIXME: does not work as intended
658
-                // re-add all remote a=ssrcs
681
+                // re-add all remote a=ssrcs _and_ a=ssrc-group
659 682
                 for (var jid in this.remotessrc) {
660 683
                     if (jid == peer || !this.remotessrc[jid][i])
661 684
                         continue;
662 685
                     sdp.media[i] += this.remotessrc[jid][i];
663 686
                 }
664
-                // and local a=ssrc lines
665
-                sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc').join('\r\n') + '\r\n';
687
+
688
+                // add local a=ssrc-group: lines
689
+                lines = SDPUtil.find_lines(localSDP.media[i], 'a=ssrc-group:');
690
+                if (lines.length != 0)
691
+                    sdp.media[i] += lines.join('\r\n') + '\r\n';
692
+
693
+                // and local a=ssrc: lines
694
+                sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc:').join('\r\n') + '\r\n';
666 695
             }
667 696
         }
668 697
         sdp.raw = sdp.session + sdp.media.join('');
@@ -864,6 +893,24 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
864 893
                 expire: self.channelExpire
865 894
             });
866 895
 
896
+            // signal (throught COLIBRI) to the bridge the SSRC groups of this
897
+            // participant
898
+            var ssrc_group_lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
899
+            var idx = 0;
900
+            ssrc_group_lines.forEach(function(line) {
901
+                idx = line.indexOf(' ');
902
+                var semantics = line.substr(0, idx).substr(13);
903
+                var ssrcs = line.substr(14 + semantics.length).split(' ');
904
+                if (ssrcs.length != 0) {
905
+                    change.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
906
+                    ssrcs.forEach(function(ssrc) {
907
+                        change.c('source', { ssrc: ssrc })
908
+                            .up();
909
+                    });
910
+                    change.up();
911
+                }
912
+            });
913
+
867 914
             var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
868 915
             rtpmap.forEach(function (val) {
869 916
                 // TODO: too much copy-paste
@@ -1028,20 +1075,33 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
1028 1075
         if (!remoteSDP.media[channel])
1029 1076
             continue;
1030 1077
 
1078
+        var lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
1079
+        if (lines.length != 0)
1080
+            // prepend ssrc-groups
1081
+            this.remotessrc[session.peerjid][channel] = lines.join('\r\n') + '\r\n';
1082
+
1031 1083
         if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
1032 1084
         {
1033
-            this.remotessrc[session.peerjid][channel] =
1085
+            if (!this.remotessrc[session.peerjid][channel])
1086
+                this.remotessrc[session.peerjid][channel] = '';
1087
+
1088
+            this.remotessrc[session.peerjid][channel] +=
1034 1089
                 SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
1035 1090
                         .join('\r\n') + '\r\n';
1036 1091
         }
1037 1092
     }
1038 1093
 
1039
-    // ACT 4: add new a=ssrc lines to local remotedescription
1094
+    // ACT 4: add new a=ssrc and s=ssrc-group lines to local remotedescription
1040 1095
     for (channel = 0; channel < this.channels[participant].length; channel++) {
1041 1096
         //if (channel == 0) continue; FIXME: does not work as intended
1042 1097
         if (!remoteSDP.media[channel])
1043 1098
             continue;
1044 1099
 
1100
+        var lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
1101
+        if (lines.length != 0)
1102
+            this.peerconnection.enqueueAddSsrc(
1103
+                channel, SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:').join('\r\n') + '\r\n');
1104
+
1045 1105
         if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
1046 1106
             this.peerconnection.enqueueAddSsrc(
1047 1107
                 channel,
@@ -1311,6 +1371,9 @@ ColibriFocus.prototype.sendTerminate = function (session, reason, text) {
1311 1371
 
1312 1372
 ColibriFocus.prototype.setRTCPTerminationStrategy = function (strategyFQN) {
1313 1373
     var self = this;
1374
+
1375
+    // TODO(gp) maybe move the RTCP termination strategy element under the
1376
+    // content or channel element.
1314 1377
     var strategyIQ = $iq({to: this.bridgejid, type: 'set'});
1315 1378
     strategyIQ.c('conference', {
1316 1379
 	    xmlns: 'http://jitsi.org/protocol/colibri',
@@ -1378,3 +1441,50 @@ ColibriFocus.prototype.setChannelLastN = function (channelLastN) {
1378 1441
     }
1379 1442
 };
1380 1443
 
1444
+/**
1445
+ * Sets the default value of the channel simulcast layer attribute in this
1446
+ * conference and updates/patches the existing channels.
1447
+ */
1448
+ColibriFocus.prototype.setReceiveSimulcastLayer = function (receiveSimulcastLayer) {
1449
+    if (('number' === typeof(receiveSimulcastLayer))
1450
+        && (this.receiveSimulcastLayer !== receiveSimulcastLayer))
1451
+    {
1452
+        // TODO(gp) be able to set the receiving simulcast layer on a per
1453
+        // sender basis.
1454
+        this.receiveSimulcastLayer = receiveSimulcastLayer;
1455
+
1456
+        // Update/patch the existing channels.
1457
+        var patch = $iq({ to: this.bridgejid, type: 'set' });
1458
+
1459
+        patch.c(
1460
+            'conference',
1461
+            { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
1462
+        patch.c('content', { name: 'video' });
1463
+        patch.c(
1464
+            'channel',
1465
+            {
1466
+                id: $(this.mychannel[1 /* video */]).attr('id'),
1467
+                'receive-simulcast-layer': this.receiveSimulcastLayer
1468
+            });
1469
+        patch.up(); // end of channel
1470
+        for (var p = 0; p < this.channels.length; p++)
1471
+        {
1472
+            patch.c(
1473
+                'channel',
1474
+                {
1475
+                    id: $(this.channels[p][1 /* video */]).attr('id'),
1476
+                    'receive-simulcast-layer': this.receiveSimulcastLayer
1477
+                });
1478
+            patch.up(); // end of channel
1479
+        }
1480
+        this.connection.sendIQ(
1481
+            patch,
1482
+            function (res) {
1483
+                console.info('Set channel simulcast receive layer succeeded:', res);
1484
+            },
1485
+            function (err) {
1486
+                console.error('Set channel simulcast receive layer failed:', err);
1487
+            });
1488
+    }
1489
+};
1490
+

+ 67
- 12
libs/strophe/strophe.jingle.adapter.js View File

@@ -128,7 +128,11 @@ dumpSDP = function(description) {
128 128
 if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
129 129
     TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
130 130
     TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
131
-    TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() { return this.peerconnection.localDescription; });
131
+    TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() {
132
+        var simulcast = new Simulcast();
133
+        var publicLocalDescription = simulcast.makeLocalDescriptionPublic(this.peerconnection.localDescription);
134
+        return publicLocalDescription;
135
+    });
132 136
     TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() { return this.peerconnection.remoteDescription; });
133 137
 }
134 138
 
@@ -149,6 +153,8 @@ TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
149 153
 
150 154
 TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
151 155
     var self = this;
156
+    var simulcast = new Simulcast();
157
+    description = simulcast.transformLocalDescription(description);
152 158
     this.trace('setLocalDescription', dumpSDP(description));
153 159
     this.peerconnection.setLocalDescription(description,
154 160
         function () {
@@ -169,6 +175,8 @@ TraceablePeerConnection.prototype.setLocalDescription = function (description, s
169 175
 
170 176
 TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
171 177
     var self = this;
178
+    var simulcast = new Simulcast();
179
+    description = simulcast.transformRemoteDescription(description);
172 180
     this.trace('setRemoteDescription', dumpSDP(description));
173 181
     this.peerconnection.setRemoteDescription(description,
174 182
         function () {
@@ -208,6 +216,16 @@ TraceablePeerConnection.prototype.addSource = function (elem) {
208 216
     $(elem).each(function (idx, content) {
209 217
         var name = $(content).attr('name');
210 218
         var lines = '';
219
+        tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
220
+            var semantics = this.getAttribute('semantics');
221
+            var ssrcs = $(this).find('>source').map(function () {
222
+                return this.getAttribute('ssrc');
223
+            }).get();
224
+
225
+            if (ssrcs.length != 0) {
226
+                lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
227
+            }
228
+        });
211 229
         tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
212 230
         tmp.each(function () {
213 231
             var ssrc = $(this).attr('ssrc');
@@ -254,6 +272,16 @@ TraceablePeerConnection.prototype.removeSource = function (elem) {
254 272
     $(elem).each(function (idx, content) {
255 273
         var name = $(content).attr('name');
256 274
         var lines = '';
275
+        tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
276
+            var semantics = this.getAttribute('semantics');
277
+            var ssrcs = $(this).find('>source').map(function () {
278
+                return this.getAttribute('ssrc');
279
+            }).get();
280
+
281
+            if (ssrcs.length != 0) {
282
+                lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
283
+            }
284
+        });
257 285
         tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
258 286
         tmp.each(function () {
259 287
             var ssrc = $(this).attr('ssrc');
@@ -413,6 +441,8 @@ TraceablePeerConnection.prototype.createAnswer = function (successCallback, fail
413 441
     this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
414 442
     this.peerconnection.createAnswer(
415 443
         function (answer) {
444
+            var simulcast = new Simulcast();
445
+            answer = simulcast.transformAnswer(answer);
416 446
             self.trace('createAnswerOnSuccess', dumpSDP(answer));
417 447
             successCallback(answer);
418 448
         },
@@ -628,18 +658,43 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
628 658
         constraints.video.mandatory.minFrameRate = fps;
629 659
     }
630 660
 
661
+    var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
662
+
631 663
     try {
632
-        RTC.getUserMedia(constraints,
633
-            function (stream) {
634
-                console.log('onUserMediaSuccess');
635
-                success_callback(stream);
636
-            },
637
-            function (error) {
638
-                console.warn('Failed to get access to local media. Error ', error);
639
-                if(failure_callback) {
640
-                    failure_callback(error);
641
-                }
642
-            });
664
+        if (config.enableSimulcast
665
+            && constraints.video
666
+            && constraints.video.chromeMediaSource !== 'screen'
667
+            && constraints.video.chromeMediaSource !== 'desktop'
668
+            && !isAndroid
669
+
670
+            // We currently do not support FF, as it doesn't have multistream support.
671
+            && !isFF) {
672
+            var simulcast = new Simulcast();
673
+            simulcast.getUserMedia(constraints, function (stream) {
674
+                    console.log('onUserMediaSuccess');
675
+                    success_callback(stream);
676
+                },
677
+                function (error) {
678
+                    console.warn('Failed to get access to local media. Error ', error);
679
+                    if (failure_callback) {
680
+                        failure_callback(error);
681
+                    }
682
+                });
683
+        } else {
684
+
685
+            RTC.getUserMedia(constraints,
686
+                function (stream) {
687
+                    console.log('onUserMediaSuccess');
688
+                    success_callback(stream);
689
+                },
690
+                function (error) {
691
+                    console.warn('Failed to get access to local media. Error ', error);
692
+                    if (failure_callback) {
693
+                        failure_callback(error);
694
+                    }
695
+                });
696
+
697
+        }
643 698
     } catch (e) {
644 699
         console.error('GUM failed: ', e);
645 700
         if(failure_callback) {

+ 89
- 0
libs/strophe/strophe.jingle.sdp.js View File

@@ -31,6 +31,15 @@ SDP.prototype.getMediaSsrcMap = function() {
31 31
             }
32 32
             channel.ssrcs[linessrc].lines.push(line);
33 33
         });
34
+        tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc-group:');
35
+        tmp.forEach(function(line){
36
+            var semantics = line.substr(0, idx).substr(13);
37
+            var ssrcs = line.substr(14 + semantics.length).split(' ');
38
+            if (ssrcs.length != 0) {
39
+                var ssrcGroup = new ChannelSsrcGroup(semantics, ssrcs);
40
+                channel.ssrcGroups.push(ssrcGroup);
41
+            }
42
+        });
34 43
     }
35 44
     return media_ssrcs;
36 45
 }
@@ -56,6 +65,32 @@ SDP.prototype.containsSSRC = function(ssrc) {
56 65
  * @param otherSdp the other SDP to check ssrc with.
57 66
  */
58 67
 SDP.prototype.getNewMedia = function(otherSdp) {
68
+
69
+    // this could be useful in Array.prototype.
70
+    function arrayEquals(array) {
71
+        // if the other array is a falsy value, return
72
+        if (!array)
73
+            return false;
74
+
75
+        // compare lengths - can save a lot of time
76
+        if (this.length != array.length)
77
+            return false;
78
+
79
+        for (var i = 0, l=this.length; i < l; i++) {
80
+            // Check if we have nested arrays
81
+            if (this[i] instanceof Array && array[i] instanceof Array) {
82
+                // recurse into the nested arrays
83
+                if (!this[i].equals(array[i]))
84
+                    return false;
85
+            }
86
+            else if (this[i] != array[i]) {
87
+                // Warning - two different object instances will never be equal: {x:20} != {x:20}
88
+                return false;
89
+            }
90
+        }
91
+        return true;
92
+    };
93
+
59 94
     var myMedia = this.getMediaSsrcMap();
60 95
     var othersMedia = otherSdp.getMediaSsrcMap();
61 96
     var newMedia = {};
@@ -77,6 +112,32 @@ SDP.prototype.getNewMedia = function(otherSdp) {
77 112
                 newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc];
78 113
             }
79 114
         })
115
+
116
+        // Look for new ssrc groups across the channels
117
+        othersChannel.ssrcGroups.forEach(function(otherSsrcGroup){
118
+
119
+            // try to match the other ssrc-group with an ssrc-group of ours
120
+            var matched = false;
121
+            for (var i = 0; i < myChannel.ssrcGroups.length; i++) {
122
+                var mySsrcGroup = myChannel.ssrcGroups[i];
123
+                if (otherSsrcGroup.semantics == mySsrcGroup
124
+                    && arrayEquals.apply(otherSsrcGroup.ssrcs, [mySsrcGroup.ssrcs])) {
125
+
126
+                    matched = true;
127
+                    break;
128
+                }
129
+            }
130
+
131
+            if (!matched) {
132
+                // Allocate channel if we've found an ssrc-group that doesn't
133
+                // exist in our channel
134
+
135
+                if(!newMedia[channelNum]){
136
+                    newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
137
+                }
138
+                newMedia[channelNum].ssrcGroups.push(otherSsrcGroup);
139
+            }
140
+        });
80 141
     });
81 142
     return newMedia;
82 143
 }
@@ -241,6 +302,22 @@ SDP.prototype.toJingle = function (elem, thecreator) {
241 302
                 tmp.xmlns = 'http://estos.de/ns/ssrc';
242 303
                 tmp.ssrc = ssrc;
243 304
                 elem.c('ssrc', tmp).up(); // ssrc is part of description
305
+
306
+                // XEP-0339 handle ssrc-group attributes
307
+                var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
308
+                ssrc_group_lines.forEach(function(line) {
309
+                    idx = line.indexOf(' ');
310
+                    var semantics = line.substr(0, idx).substr(13);
311
+                    var ssrcs = line.substr(14 + semantics.length).split(' ');
312
+                    if (ssrcs.length != 0) {
313
+                        elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
314
+                        ssrcs.forEach(function(ssrc) {
315
+                            elem.c('source', { ssrc: ssrc })
316
+                                .up();
317
+                        });
318
+                        elem.up();
319
+                    }
320
+                });
244 321
             }
245 322
 
246 323
             if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
@@ -578,6 +655,18 @@ SDP.prototype.jingle2media = function (content) {
578 655
         media += SDPUtil.candidateFromJingle(this);
579 656
     });
580 657
 
658
+    // XEP-0339 handle ssrc-group attributes
659
+    tmp = content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
660
+        var semantics = this.getAttribute('semantics');
661
+        var ssrcs = $(this).find('>source').map(function() {
662
+            return this.getAttribute('ssrc');
663
+        }).get();
664
+
665
+        if (ssrcs.length != 0) {
666
+            media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
667
+        }
668
+    });
669
+
581 670
     tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
582 671
     tmp.each(function () {
583 672
         var ssrc = this.getAttribute('ssrc');

+ 17
- 0
libs/strophe/strophe.jingle.sdp.util.js View File

@@ -15,6 +15,17 @@ function ChannelSsrc(ssrc, type) {
15 15
     this.lines = [];
16 16
 }
17 17
 
18
+/**
19
+ * Class holds a=ssrc-group: lines
20
+ * @param semantics
21
+ * @param ssrcs
22
+ * @constructor
23
+ */
24
+function ChannelSsrcGroup(semantics, ssrcs, line) {
25
+    this.semantics = semantics;
26
+    this.ssrcs = ssrcs;
27
+}
28
+
18 29
 /**
19 30
  * Helper class represents media channel. Is a container for ChannelSsrc, holds channel idx and media type.
20 31
  * @param channelNumber channel idx in SDP media array.
@@ -36,6 +47,12 @@ function MediaChannel(channelNumber, mediaType) {
36 47
      * The maps of ssrc numbers to ChannelSsrc objects.
37 48
      */
38 49
     this.ssrcs = {};
50
+
51
+    /**
52
+     * The array of ChannelSsrcGroup objects.
53
+     * @type {Array}
54
+     */
55
+    this.ssrcGroups = [];
39 56
 }
40 57
 
41 58
 SDPUtil = {

+ 6
- 1
libs/strophe/strophe.jingle.session.js View File

@@ -119,6 +119,8 @@ JingleSession.prototype.accept = function () {
119 119
         // FIXME: change any inactive to sendrecv or whatever they were originally
120 120
         pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
121 121
     }
122
+    var simulcast = new Simulcast();
123
+    pranswer = simulcast.makeLocalDescriptionPublic(pranswer);
122 124
     var prsdp = new SDP(pranswer.sdp);
123 125
     var accept = $iq({to: this.peerjid,
124 126
         type: 'set'})
@@ -565,7 +567,10 @@ JingleSession.prototype.createdAnswer = function (sdp, provisional) {
565 567
                     initiator: this.initiator,
566 568
                     responder: this.responder,
567 569
                     sid: this.sid });
568
-            this.localSDP.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
570
+            var simulcast = new Simulcast();
571
+            var publicLocalDesc = simulcast.makeLocalDescriptionPublic(sdp);
572
+            var publicLocalSDP = new SDP(publicLocalDesc.sdp);
573
+            publicLocalSDP.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
569 574
             this.connection.sendIQ(accept,
570 575
                 function () {
571 576
                     var ack = {};

+ 669
- 0
simulcast.js View File

@@ -0,0 +1,669 @@
1
+/*jslint plusplus: true */
2
+/*jslint nomen: true*/
3
+
4
+/**
5
+ * Created by gp on 11/08/14.
6
+ */
7
+function Simulcast() {
8
+    "use strict";
9
+
10
+    // TODO(gp) split the Simulcast class in two classes : NativeSimulcast and ClassicSimulcast.
11
+    this.debugLvl = 1;
12
+}
13
+
14
+(function () {
15
+    "use strict";
16
+    // global state for all transformers.
17
+    var localExplosionMap = {}, localVideoSourceCache, emptyCompoundIndex,
18
+        remoteMaps = {
19
+            msid2Quality: {},
20
+            ssrc2Msid: {},
21
+            receivingVideoStreams: {}
22
+        }, localMaps = {
23
+            msids: [],
24
+            msid2ssrc: {}
25
+        };
26
+
27
+    Simulcast.prototype._generateGuid = (function () {
28
+        function s4() {
29
+            return Math.floor((1 + Math.random()) * 0x10000)
30
+                .toString(16)
31
+                .substring(1);
32
+        }
33
+
34
+        return function () {
35
+            return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
36
+                s4() + '-' + s4() + s4() + s4();
37
+        };
38
+    }());
39
+
40
+    Simulcast.prototype._cacheVideoSources = function (lines) {
41
+        localVideoSourceCache = this._getVideoSources(lines);
42
+    };
43
+
44
+    Simulcast.prototype._restoreVideoSources = function (lines) {
45
+        this._replaceVideoSources(lines, localVideoSourceCache);
46
+    };
47
+
48
+    Simulcast.prototype._replaceVideoSources = function (lines, videoSources) {
49
+
50
+        var i, inVideo = false, index = -1, howMany = 0;
51
+
52
+        if (this.debugLvl) {
53
+            console.info('Replacing video sources...');
54
+        }
55
+
56
+        for (i = 0; i < lines.length; i++) {
57
+            if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
58
+                // Out of video.
59
+                break;
60
+            }
61
+
62
+            if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
63
+                // In video.
64
+                inVideo = true;
65
+            }
66
+
67
+            if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:'
68
+                    || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) {
69
+
70
+                if (index === -1) {
71
+                    index = i;
72
+                }
73
+
74
+                howMany++;
75
+            }
76
+        }
77
+
78
+        //  efficiency baby ;)
79
+        lines.splice.apply(lines,
80
+            [index, howMany].concat(videoSources));
81
+
82
+    };
83
+
84
+    Simulcast.prototype._getVideoSources = function (lines) {
85
+        var i, inVideo = false, sb = [];
86
+
87
+        if (this.debugLvl) {
88
+            console.info('Getting video sources...');
89
+        }
90
+
91
+        for (i = 0; i < lines.length; i++) {
92
+            if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
93
+                // Out of video.
94
+                break;
95
+            }
96
+
97
+            if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
98
+                // In video.
99
+                inVideo = true;
100
+            }
101
+
102
+            if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
103
+                // In SSRC.
104
+                sb.push(lines[i]);
105
+            }
106
+
107
+            if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
108
+                sb.push(lines[i]);
109
+            }
110
+        }
111
+
112
+        return sb;
113
+    };
114
+
115
+    Simulcast.prototype._parseMedia = function (lines, mediatypes) {
116
+        var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc,
117
+            ssrc_attribute, group, semantics, skip;
118
+
119
+        if (this.debugLvl) {
120
+            console.info('Parsing media sources...');
121
+        }
122
+
123
+        for (i = 0; i < lines.length; i++) {
124
+            if (lines[i].substring(0, 'm='.length) === 'm=') {
125
+
126
+                type = lines[i]
127
+                    .substr('m='.length, lines[i].indexOf(' ') - 'm='.length);
128
+                skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1;
129
+
130
+                if (!skip) {
131
+                    cur_media = {
132
+                        'type': type,
133
+                        'sources': {},
134
+                        'groups': []
135
+                    };
136
+
137
+                    res.push(cur_media);
138
+                }
139
+
140
+            } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
141
+
142
+                idx = lines[i].indexOf(' ');
143
+                ssrc = lines[i].substring('a=ssrc:'.length, idx);
144
+                if (cur_media.sources[ssrc] === undefined) {
145
+                    cur_ssrc = {'ssrc': ssrc};
146
+                    cur_media.sources[ssrc] = cur_ssrc;
147
+                }
148
+
149
+                ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0];
150
+                cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1];
151
+
152
+                if (cur_media.base === undefined) {
153
+                    cur_media.base = cur_ssrc;
154
+                }
155
+
156
+            } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
157
+                idx = lines[i].indexOf(' ');
158
+                semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length);
159
+                ssrcs = lines[i].substr(idx).trim().split(' ');
160
+                group = {
161
+                    'semantics': semantics,
162
+                    'ssrcs': ssrcs
163
+                };
164
+                cur_media.groups.push(group);
165
+            } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' ||
166
+                                    lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' ||
167
+                                    lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' ||
168
+                                    lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) {
169
+
170
+                cur_media.direction = lines[i].substring('a='.length, 8);
171
+            }
172
+        }
173
+
174
+        return res;
175
+    };
176
+
177
+    // Returns a random integer between min (included) and max (excluded)
178
+    // Using Math.round() will give you a non-uniform distribution!
179
+    Simulcast.prototype._generateRandomSSRC = function () {
180
+        var min = 0, max = 0xffffffff;
181
+        return Math.floor(Math.random() * (max - min)) + min;
182
+    };
183
+
184
+    function CompoundIndex(obj) {
185
+        if (obj !== undefined) {
186
+            this.row = obj.row;
187
+            this.column = obj.column;
188
+        }
189
+    }
190
+
191
+    emptyCompoundIndex = new CompoundIndex();
192
+
193
+    Simulcast.prototype._indexOfArray = function (needle, haystack, start) {
194
+        var length = haystack.length, idx, i;
195
+
196
+        if (!start) {
197
+            start = 0;
198
+        }
199
+
200
+        for (i = start; i < length; i++) {
201
+            idx = haystack[i].indexOf(needle);
202
+            if (idx !== -1) {
203
+                return new CompoundIndex({row: i, column: idx});
204
+            }
205
+        }
206
+        return emptyCompoundIndex;
207
+    };
208
+
209
+    Simulcast.prototype._removeSimulcastGroup = function (lines) {
210
+        var i;
211
+
212
+        for (i = lines.length - 1; i >= 0; i--) {
213
+            if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) {
214
+                lines.splice(i, 1);
215
+            }
216
+        }
217
+    };
218
+
219
+    Simulcast.prototype._explodeLocalSimulcastSources = function (lines) {
220
+        var sb, msid, sid, tid, videoSources, self;
221
+
222
+        if (this.debugLvl) {
223
+            console.info('Exploding local video sources...');
224
+        }
225
+
226
+        videoSources = this._parseMedia(lines, ['video'])[0];
227
+
228
+        self = this;
229
+        if (videoSources.groups && videoSources.groups.length !== 0) {
230
+            videoSources.groups.forEach(function (group) {
231
+                if (group.semantics === 'SIM') {
232
+                    group.ssrcs.forEach(function (ssrc) {
233
+
234
+                        // Get the msid for this ssrc..
235
+                        if (localExplosionMap[ssrc]) {
236
+                            // .. either from the explosion map..
237
+                            msid = localExplosionMap[ssrc];
238
+                        } else {
239
+                            // .. or generate a new one (msid).
240
+                            sid = videoSources.sources[ssrc].msid
241
+                               .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
242
+
243
+                            tid = self._generateGuid();
244
+                            msid = [sid, tid].join(' ');
245
+                            localExplosionMap[ssrc] = msid;
246
+                        }
247
+
248
+                        // Assign it to the source object.
249
+                        videoSources.sources[ssrc].msid = msid;
250
+
251
+                        // TODO(gp) Change the msid of associated sources.
252
+                    });
253
+                }
254
+            });
255
+        }
256
+
257
+        sb = this._compileVideoSources(videoSources);
258
+
259
+        this._replaceVideoSources(lines, sb);
260
+    };
261
+
262
+    Simulcast.prototype._groupLocalVideoSources = function (lines) {
263
+        var sb, videoSources, ssrcs = [], ssrc;
264
+
265
+        if (this.debugLvl) {
266
+            console.info('Grouping local video sources...');
267
+        }
268
+
269
+        videoSources = this._parseMedia(lines, ['video'])[0];
270
+
271
+        for (ssrc in videoSources.sources) {
272
+            // jitsi-meet destroys/creates streams at various places causing
273
+            // the original local stream ids to change. The only thing that
274
+            // remains unchanged is the trackid.
275
+            localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
276
+        }
277
+
278
+        // TODO(gp) add only "free" sources.
279
+        localMaps.msids.forEach(function (msid) {
280
+            ssrcs.push(localMaps.msid2ssrc[msid]);
281
+        });
282
+
283
+        if (!videoSources.groups) {
284
+            videoSources.groups = [];
285
+        }
286
+
287
+        videoSources.groups.push({
288
+            'semantics': 'SIM',
289
+            'ssrcs': ssrcs
290
+        });
291
+
292
+        sb = this._compileVideoSources(videoSources);
293
+
294
+        this._replaceVideoSources(lines, sb);
295
+    };
296
+
297
+    Simulcast.prototype._appendSimulcastGroup = function (lines) {
298
+        var videoSources, ssrcGroup, simSSRC, numOfSubs = 3, i, sb, msid;
299
+
300
+        if (this.debugLvl) {
301
+            console.info('Appending simulcast group...');
302
+        }
303
+
304
+        // Get the primary SSRC information.
305
+        videoSources = this._parseMedia(lines, ['video'])[0];
306
+
307
+        // Start building the SIM SSRC group.
308
+        ssrcGroup = ['a=ssrc-group:SIM'];
309
+
310
+        // The video source buffer.
311
+        sb = [];
312
+
313
+        // Create the simulcast sub-streams.
314
+        for (i = 0; i < numOfSubs; i++) {
315
+            // TODO(gp) prevent SSRC collision.
316
+            simSSRC = this._generateRandomSSRC();
317
+            ssrcGroup.push(simSSRC);
318
+
319
+            sb.splice.apply(sb, [sb.length, 0].concat(
320
+                [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
321
+                    ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
322
+            ));
323
+
324
+            if (this.debugLvl) {
325
+                console.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
326
+            }
327
+        }
328
+
329
+        // Add the group sim layers.
330
+        sb.splice(0, 0, ssrcGroup.join(' '))
331
+
332
+        this._replaceVideoSources(lines, sb);
333
+    };
334
+
335
+    // Does the actual patching.
336
+    Simulcast.prototype._ensureSimulcastGroup = function (lines) {
337
+        if (this.debugLvl) {
338
+            console.info('Ensuring simulcast group...');
339
+        }
340
+
341
+        if (this._indexOfArray('a=ssrc-group:SIM', lines) === emptyCompoundIndex) {
342
+            this._appendSimulcastGroup(lines);
343
+            this._cacheVideoSources(lines);
344
+        } else {
345
+            // verify that the ssrcs participating in the SIM group are present
346
+            // in the SDP (needed for presence).
347
+            this._restoreVideoSources(lines);
348
+        }
349
+    };
350
+
351
+    Simulcast.prototype._ensureGoogConference = function (lines) {
352
+        var sb;
353
+        if (this.debugLvl) {
354
+            console.info('Ensuring x-google-conference flag...')
355
+        }
356
+
357
+        if (this._indexOfArray('a=x-google-flag:conference', lines) === emptyCompoundIndex) {
358
+            // Add the google conference flag
359
+            sb = this._getVideoSources(lines);
360
+            sb = ['a=x-google-flag:conference'].concat(sb);
361
+            this._replaceVideoSources(lines, sb);
362
+        }
363
+    };
364
+
365
+    Simulcast.prototype._compileVideoSources = function (videoSources) {
366
+        var sb = [], ssrc, addedSSRCs = [];
367
+
368
+        if (this.debugLvl) {
369
+            console.info('Compiling video sources...');
370
+        }
371
+
372
+        // Add the groups
373
+        if (videoSources.groups && videoSources.groups.length !== 0) {
374
+            videoSources.groups.forEach(function (group) {
375
+                if (group.ssrcs && group.ssrcs.length !== 0) {
376
+                    sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' '));
377
+
378
+                    // if (group.semantics !== 'SIM') {
379
+                        group.ssrcs.forEach(function (ssrc) {
380
+                            addedSSRCs.push(ssrc);
381
+                            sb.splice.apply(sb, [sb.length, 0].concat([
382
+                                ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
383
+                                ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
384
+                        });
385
+                    //}
386
+                }
387
+            });
388
+        }
389
+
390
+        // Then add any free sources.
391
+        if (videoSources.sources) {
392
+            for (ssrc in videoSources.sources) {
393
+                if (addedSSRCs.indexOf(ssrc) === -1) {
394
+                    sb.splice.apply(sb, [sb.length, 0].concat([
395
+                        ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
396
+                        ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
397
+                }
398
+            }
399
+        }
400
+
401
+        return sb;
402
+    };
403
+
404
+    Simulcast.prototype.transformAnswer = function (desc) {
405
+        if (config.enableSimulcast && config.useNativeSimulcast) {
406
+
407
+            var sb = desc.sdp.split('\r\n');
408
+
409
+            // Even if we have enabled native simulcasting previously
410
+            // (with a call to SLD with an appropriate SDP, for example),
411
+            // createAnswer seems to consistently generate incomplete SDP
412
+            // with missing SSRCS.
413
+            //
414
+            // So, subsequent calls to SLD will have missing SSRCS and presence
415
+            // won't have the complete list of SRCs.
416
+            this._ensureSimulcastGroup(sb);
417
+
418
+            desc = new RTCSessionDescription({
419
+                type: desc.type,
420
+                sdp: sb.join('\r\n')
421
+            });
422
+
423
+            if (this.debugLvl && this.debugLvl > 1) {
424
+                console.info('Transformed answer');
425
+                console.info(desc.sdp);
426
+            }
427
+        }
428
+
429
+        return desc;
430
+    };
431
+
432
+    Simulcast.prototype.makeLocalDescriptionPublic = function (desc) {
433
+        var sb;
434
+
435
+        if (!desc || desc == null)
436
+            return desc;
437
+
438
+        if (config.enableSimulcast) {
439
+
440
+            if (config.useNativeSimulcast) {
441
+                sb = desc.sdp.split('\r\n');
442
+
443
+                this._explodeLocalSimulcastSources(sb);
444
+
445
+                desc = new RTCSessionDescription({
446
+                    type: desc.type,
447
+                    sdp: sb.join('\r\n')
448
+                });
449
+
450
+                if (this.debugLvl && this.debugLvl > 1) {
451
+                    console.info('Exploded local video sources');
452
+                    console.info(desc.sdp);
453
+                }
454
+            } else {
455
+                sb = desc.sdp.split('\r\n');
456
+
457
+                this._groupLocalVideoSources(sb);
458
+
459
+                desc = new RTCSessionDescription({
460
+                    type: desc.type,
461
+                    sdp: sb.join('\r\n')
462
+                });
463
+
464
+                if (this.debugLvl && this.debugLvl > 1) {
465
+                    console.info('Grouped local video sources');
466
+                    console.info(desc.sdp);
467
+                }
468
+            }
469
+        }
470
+
471
+        return desc;
472
+    };
473
+
474
+    Simulcast.prototype._ensureOrder = function (lines) {
475
+        var videoSources, sb;
476
+
477
+        videoSources = this._parseMedia(lines, ['video'])[0];
478
+        sb = this._compileVideoSources(videoSources);
479
+
480
+        this._replaceVideoSources(lines, sb);
481
+    };
482
+
483
+    Simulcast.prototype.transformBridgeDescription = function (desc) {
484
+        if (config.enableSimulcast && config.useNativeSimulcast) {
485
+
486
+            var sb = desc.sdp.split('\r\n');
487
+
488
+            this._ensureGoogConference(sb);
489
+
490
+            desc = new RTCSessionDescription({
491
+                type: desc.type,
492
+                sdp: sb.join('\r\n')
493
+            });
494
+
495
+            if (this.debugLvl && this.debugLvl > 1) {
496
+                console.info('Transformed bridge description');
497
+                console.info(desc.sdp);
498
+            }
499
+        }
500
+
501
+        return desc;
502
+    };
503
+
504
+    Simulcast.prototype._updateRemoteMaps = function (lines) {
505
+        var remoteVideoSources = this._parseMedia(lines, ['video'])[0], videoSource, quality;
506
+
507
+        if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) {
508
+            remoteVideoSources.groups.forEach(function (group) {
509
+                if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) {
510
+                    quality = 0;
511
+                    group.ssrcs.forEach(function (ssrc) {
512
+                        videoSource = remoteVideoSources.sources[ssrc];
513
+                        remoteMaps.msid2Quality[videoSource.msid] = quality++;
514
+                        remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid;
515
+                    });
516
+                }
517
+            });
518
+        }
519
+    };
520
+
521
+    Simulcast.prototype.transformLocalDescription = function (desc) {
522
+        if (config.enableSimulcast && !config.useNativeSimulcast) {
523
+
524
+            var sb = desc.sdp.split('\r\n');
525
+
526
+            this._removeSimulcastGroup(sb);
527
+
528
+            desc = new RTCSessionDescription({
529
+                type: desc.type,
530
+                sdp: sb.join('\r\n')
531
+            });
532
+
533
+            if (this.debugLvl && this.debugLvl > 1) {
534
+                console.info('Transformed local description');
535
+                console.info(desc.sdp);
536
+            }
537
+        }
538
+
539
+        return desc;
540
+    };
541
+
542
+    Simulcast.prototype.transformRemoteDescription = function (desc) {
543
+        if (config.enableSimulcast) {
544
+
545
+            var sb = desc.sdp.split('\r\n');
546
+
547
+            this._updateRemoteMaps(sb);
548
+            this._removeSimulcastGroup(sb); // NOTE(gp) this needs to be called after updateRemoteMaps!
549
+            this._ensureGoogConference(sb);
550
+
551
+            desc = new RTCSessionDescription({
552
+                type: desc.type,
553
+                sdp: sb.join('\r\n')
554
+            });
555
+
556
+            if (this.debugLvl && this.debugLvl > 1) {
557
+                console.info('Transformed remote description');
558
+                console.info(desc.sdp);
559
+            }
560
+        }
561
+
562
+        return desc;
563
+    };
564
+
565
+    Simulcast.prototype.setReceivingVideoStream = function (ssrc) {
566
+        var receivingTrack = remoteMaps.ssrc2Msid[ssrc],
567
+            msidParts = receivingTrack.split(' ');
568
+
569
+        remoteMaps.receivingVideoStreams[msidParts[0]] = msidParts[1];
570
+    };
571
+
572
+    Simulcast.prototype.getReceivingVideoStream = function (stream) {
573
+        var tracks, track, i, electedTrack, msid, quality = 1, receivingTrackId;
574
+
575
+        if (config.enableSimulcast) {
576
+
577
+            if (remoteMaps.receivingVideoStreams[stream.id])
578
+            {
579
+                receivingTrackId = remoteMaps.receivingVideoStreams[stream.id];
580
+                tracks = stream.getVideoTracks();
581
+                for (i = 0; i < tracks.length; i++) {
582
+                    if (receivingTrackId === tracks[i].id) {
583
+                        electedTrack = tracks[i];
584
+                        break;
585
+                    }
586
+                }
587
+            }
588
+
589
+            if (!electedTrack) {
590
+                tracks = stream.getVideoTracks();
591
+                for (i = 0; i < tracks.length; i++) {
592
+                    track = tracks[i];
593
+                    msid = [stream.id, track.id].join(' ');
594
+                    if (remoteMaps.msid2Quality[msid] === quality) {
595
+                        electedTrack = track;
596
+                        break;
597
+                    }
598
+                }
599
+            }
600
+        }
601
+
602
+        return (electedTrack)
603
+            ? new webkitMediaStream([electedTrack])
604
+            : stream;
605
+    };
606
+
607
+    Simulcast.prototype.getUserMedia = function (constraints, success, err) {
608
+
609
+        // TODO(gp) what if we request a resolution not supported by the hardware?
610
+        // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
611
+        var lqConstraints = {
612
+            audio: false,
613
+            video: {
614
+                mandatory: {
615
+                    maxWidth: 320,
616
+                    maxHeight: 180
617
+                }
618
+            }
619
+        };
620
+
621
+        if (config.enableSimulcast && !config.useNativeSimulcast) {
622
+
623
+            // NOTE(gp) if we request the lq stream first webkitGetUserMedia fails randomly. Tested with Chrome 37.
624
+
625
+            navigator.webkitGetUserMedia(constraints, function (hqStream) {
626
+
627
+                // reset local maps.
628
+                localMaps.msids = [];
629
+                localMaps.msid2ssrc = {};
630
+
631
+                // add hq trackid to local map
632
+                localMaps.msids.push(hqStream.getVideoTracks()[0].id);
633
+
634
+                navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
635
+
636
+                    // add lq trackid to local map
637
+                    localMaps.msids.push(lqStream.getVideoTracks()[0].id);
638
+
639
+                    hqStream.addTrack(lqStream.getVideoTracks()[0]);
640
+                    success(hqStream);
641
+                }, err);
642
+            }, err);
643
+        } else {
644
+
645
+            // There's nothing special to do for native simulcast, so just do a normal GUM.
646
+
647
+            navigator.webkitGetUserMedia(constraints, function (hqStream) {
648
+
649
+                // reset local maps.
650
+                localMaps.msids = [];
651
+                localMaps.msid2ssrc = {};
652
+
653
+                // add hq stream to local map
654
+                localMaps.msids.push(hqStream.getVideoTracks()[0].id);
655
+
656
+                success(hqStream);
657
+            }, err);
658
+        }
659
+    };
660
+
661
+    Simulcast.prototype.getRemoteVideoStreamIdBySSRC = function (primarySSRC) {
662
+        return remoteMaps.ssrc2Msid[primarySSRC];
663
+    };
664
+
665
+    Simulcast.prototype.parseMedia = function (desc, mediatypes) {
666
+        var lines = desc.sdp.split('\r\n');
667
+        return this._parseMedia(lines, mediatypes);
668
+    };
669
+}());

+ 85
- 2
videolayout.js View File

@@ -381,7 +381,9 @@ var VideoLayout = (function (my) {
381 381
             // If the container is currently visible we attach the stream.
382 382
             if (!isVideo
383 383
                 || (container.offsetParent !== null && isVideo)) {
384
-                RTC.attachMediaStream(sel, stream);
384
+                var simulcast = new Simulcast();
385
+                var videoStream = simulcast.getReceivingVideoStream(stream);
386
+                RTC.attachMediaStream(sel, videoStream);
385 387
 
386 388
                 if (isVideo)
387 389
                     waitForRemoteVideo(sel, thessrc, stream);
@@ -1248,7 +1250,9 @@ var VideoLayout = (function (my) {
1248 1250
                             && mediaStream.type === mediaStream.VIDEO_TYPE) {
1249 1251
                             var sel = $('#participant_' + resourceJid + '>video');
1250 1252
 
1251
-                            RTC.attachMediaStream(sel, mediaStream.stream);
1253
+                            var simulcast = new Simulcast();
1254
+                            var videoStream = simulcast.getReceivingVideoStream(mediaStream.stream);
1255
+                            RTC.attachMediaStream(sel, videoStream);
1252 1256
                             waitForRemoteVideo(
1253 1257
                                     sel,
1254 1258
                                     mediaStream.ssrc,
@@ -1288,5 +1292,84 @@ var VideoLayout = (function (my) {
1288 1292
         }
1289 1293
     });
1290 1294
 
1295
+    /**
1296
+     * On simulcast layers changed event.
1297
+     */
1298
+    $(document).bind('simulcastlayerschanged', function (event, endpointSimulcastLayers) {
1299
+        var simulcast = new Simulcast();
1300
+        endpointSimulcastLayers.forEach(function (esl) {
1301
+
1302
+            var primarySSRC = esl.simulcastLayer.primarySSRC;
1303
+            simulcast.setReceivingVideoStream(primarySSRC);
1304
+            var msid = simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC);
1305
+
1306
+            // Get session and stream from msid.
1307
+            var session, electedStream;
1308
+            var i, j, k;
1309
+            if (connection.jingle) {
1310
+                var keys = Object.keys(connection.jingle.sessions);
1311
+                for (i = 0; i < keys.length; i++) {
1312
+                    var sid = keys[i];
1313
+
1314
+                    if (electedStream) {
1315
+                        // stream found, stop.
1316
+                        break;
1317
+                    }
1318
+
1319
+                    session = connection.jingle.sessions[sid];
1320
+                    if (session.remoteStreams) {
1321
+                        for (j = 0; j < session.remoteStreams.length; j++) {
1322
+                            var remoteStream = session.remoteStreams[j];
1323
+
1324
+                            if (electedStream) {
1325
+                                // stream found, stop.
1326
+                                break;
1327
+                            }
1328
+                            var tracks = remoteStream.getVideoTracks();
1329
+                            if (tracks) {
1330
+                                for (k = 0; k < tracks.length; k++) {
1331
+                                    var track = tracks[k];
1332
+
1333
+                                    if (msid === [remoteStream.id, track.id].join(' ')) {
1334
+                                        electedStream = new webkitMediaStream([track]);
1335
+                                        // stream found, stop.
1336
+                                        break;
1337
+                                    }
1338
+                                }
1339
+                            }
1340
+                        }
1341
+                    }
1342
+                }
1343
+            }
1344
+
1345
+            if (session && electedStream) {
1346
+                console.info('Switching simulcast substream.');
1347
+                console.info([esl, primarySSRC, msid, session, electedStream]);
1348
+
1349
+                var msidParts = msid.split(' ');
1350
+                var selRemoteVideo = $(['#', 'remoteVideo_', session.sid, '_', msidParts[0]].join(''));
1351
+
1352
+                var updateLargeVideo = (ssrc2jid[videoSrcToSsrc[selRemoteVideo.attr('src')]]
1353
+                    == ssrc2jid[videoSrcToSsrc[$('#largeVideo').attr('src')]]);
1354
+                var updateFocusedVideoSrc = (selRemoteVideo.attr('src') == focusedVideoSrc);
1355
+
1356
+                var electedStreamUrl = webkitURL.createObjectURL(electedStream);
1357
+                selRemoteVideo.attr('src', electedStreamUrl);
1358
+                videoSrcToSsrc[selRemoteVideo.attr('src')] = primarySSRC;
1359
+
1360
+                if (updateLargeVideo) {
1361
+                    VideoLayout.updateLargeVideo(electedStreamUrl);
1362
+                }
1363
+
1364
+                if (updateFocusedVideoSrc) {
1365
+                    focusedVideoSrc = electedStreamUrl;
1366
+                }
1367
+
1368
+            } else {
1369
+                console.error('Could not find a stream or a session.');
1370
+            }
1371
+        });
1372
+    });
1373
+
1291 1374
     return my;
1292 1375
 }(VideoLayout || {}));

Loading…
Cancel
Save