Bläddra i källkod

Makes it possible to switch video streams during the session. Adds desktop sharing feature for chrome.

j8
paweldomas 11 år sedan
förälder
incheckning
3e34df8730

+ 14
- 0
app.js Visa fil

@@ -166,6 +166,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
166 166
         var sess = connection.jingle.sessions[sid];
167 167
         if (data.stream.id === 'mixedmslabel') return;
168 168
         videoTracks = data.stream.getVideoTracks();
169
+        console.log("waiting..", videoTracks, selector[0]);
169 170
         if (videoTracks.length === 0 || selector[0].currentTime > 0) {
170 171
             RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
171 172
             $(document).trigger('callactive.jingle', [selector, sid]);
@@ -437,6 +438,8 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) {
437 438
     });
438 439
     console.log('new ssrcs', newssrcs);
439 440
 
441
+    // Have to clear presence map to get rid of removed streams
442
+    connection.emuc.clearPresenceMedia();
440 443
     var i = 0;
441 444
     Object.keys(newssrcs).forEach(function (mtype) {
442 445
         i++;
@@ -542,6 +545,14 @@ $(document).bind('left.muc', function (event, jid) {
542 545
 });
543 546
 
544 547
 $(document).bind('presence.muc', function (event, jid, info, pres) {
548
+
549
+    // Remove old ssrcs coming from the jid
550
+    Object.keys(ssrc2jid).forEach(function(ssrc){
551
+       if(ssrc2jid[ssrc] == jid){
552
+           delete ssrc2jid[ssrc];
553
+       }
554
+    });
555
+
545 556
     $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
546 557
         //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
547 558
         ssrc2jid[ssrc.getAttribute('ssrc')] = jid;
@@ -1191,6 +1202,9 @@ function showToolbar() {
1191 1202
 //        TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
1192 1203
 //        $('#settingsButton').css({visibility:"visible"});
1193 1204
     }
1205
+    if(isDesktopSharingEnabled()) {
1206
+        $('#desktopsharing').css({display:"inline"});
1207
+    }
1194 1208
 }
1195 1209
 
1196 1210
 /*

+ 2
- 1
config.js Visa fil

@@ -8,5 +8,6 @@ var config = {
8 8
 //  useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server
9 9
 //  useIPv6: true, // ipv6 support. use at your own risk
10 10
     useNicks: false,
11
-    bosh: '//lambada.jitsi.net/http-bind' // FIXME: use xep-0156 for that
11
+    bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
12
+    chromeDesktopSharing: false // Desktop sharing is disabled by default
12 13
 };

+ 77
- 0
desktopsharing.js Visa fil

@@ -0,0 +1,77 @@
1
+/**
2
+ * Indicates that desktop stream is currently in use(for toggle purpose).
3
+ * @type {boolean}
4
+ */
5
+var isUsingScreenStream = false;
6
+/**
7
+ * Indicates that switch stream operation is in progress and prevent from triggering new events.
8
+ * @type {boolean}
9
+ */
10
+var switchInProgress = false;
11
+
12
+/**
13
+ * @returns {boolean} <tt>true</tt> if desktop sharing feature is available and enabled.
14
+ */
15
+function isDesktopSharingEnabled() {
16
+    // Desktop sharing must be enabled in config and works on chrome only.
17
+    // Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled.
18
+    return config.chromeDesktopSharing && RTC.browser == 'chrome';
19
+}
20
+
21
+/*
22
+ * Toggles screen sharing.
23
+ */
24
+function toggleScreenSharing() {
25
+    if (!(connection && connection.connected
26
+        && !switchInProgress
27
+        && getConferenceHandler().peerconnection.signalingState == 'stable'
28
+        && getConferenceHandler().peerconnection.iceConnectionState == 'connected')) {
29
+        return;
30
+    }
31
+    switchInProgress = true;
32
+
33
+    // Only the focus is able to set a shared key.
34
+    if(!isUsingScreenStream)
35
+    {
36
+        // Enable screen stream
37
+        getUserMediaWithConstraints(
38
+            ['screen'],
39
+            function(stream){
40
+                isUsingScreenStream = true;
41
+                gotScreenStream(stream);
42
+            },
43
+            getSwitchStreamFailed
44
+        );
45
+    } else {
46
+        // Disable screen stream
47
+        getUserMediaWithConstraints(
48
+            ['video'],
49
+            function(stream) {
50
+                isUsingScreenStream = false;
51
+                gotScreenStream(stream);
52
+            },
53
+            getSwitchStreamFailed, config.resolution || '360'
54
+        );
55
+    }
56
+}
57
+
58
+function getSwitchStreamFailed(error) {
59
+    console.error("Failed to obtain the stream to switch to", error);
60
+    switchInProgress = false;
61
+}
62
+
63
+function gotScreenStream(stream) {
64
+    var oldStream = connection.jingle.localVideo;
65
+
66
+    change_local_video(stream);
67
+
68
+    // FIXME: will block switchInProgress on true value in case of exception
69
+    getConferenceHandler().switchStreams(stream, oldStream, onDesktopStreamEnabled);
70
+}
71
+
72
+function onDesktopStreamEnabled() {
73
+    // Wait a moment before enabling the button
74
+    window.setTimeout(function() {
75
+        switchInProgress = false;
76
+    }, 3000);
77
+}

+ 6
- 0
index.html Visa fil

@@ -6,6 +6,7 @@
6 6
     <script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script>
7 7
     <script src="libs/strophe/strophe.jingle.js?v=1"></script>
8 8
     <script src="libs/strophe/strophe.jingle.sdp.js?v=1"></script>
9
+    <script src="libs/strophe/strophe.jingle.sdp.util.js?v=1"></script>
9 10
     <script src="libs/strophe/strophe.jingle.sessionbase.js?v=1"></script>
10 11
     <script src="libs/strophe/strophe.jingle.session.js?v=1"></script>
11 12
     <script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
@@ -15,6 +16,7 @@
15 16
     <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
16 17
     <script src="app.js?v=23"></script><!-- application logic -->
17 18
     <script src="chat.js?v=3"></script><!-- chat logic -->
19
+    <script src="desktopsharing.js?v=1"></script><!-- desktop sharing logic -->
18 20
     <script src="util.js?v=2"></script><!-- utility functions -->
19 21
     <script src="etherpad.js?v=5"></script><!-- etherpad plugin -->
20 22
     <script src="smileys.js?v=1"></script><!-- smiley images -->
@@ -54,6 +56,10 @@
54 56
                 <a class="button" onclick='Etherpad.toggleEtherpad(0);'><i title="Open shared document" class="fa fa-file-text fa-lg"></i></a>
55 57
             </span>
56 58
             <div class="header_button_separator"></div>
59
+            <span id="desktopsharing" style="display: none">
60
+                <a class="button" onclick="toggleScreenSharing();"><i title="Share screen" class="fa fa-desktop fa-lg"></i></a>
61
+                <div class="header_button_separator"></div>
62
+            </span>
57 63
             <a class="button" onclick='toggleFullScreen();'><i title="Enter / Exit Full Screen" class="fa fa-arrows-alt fa-lg"></i></a>
58 64
         </span>
59 65
     </div>

+ 71
- 53
libs/colibri/colibri.focus.js Visa fil

@@ -38,7 +38,7 @@
38 38
 ColibriFocus.prototype = Object.create(SessionBase.prototype);
39 39
 function ColibriFocus(connection, bridgejid) {
40 40
 
41
-    SessionBase.call(this, connection);
41
+    SessionBase.call(this, connection, Math.random().toString(36).substr(2, 12));
42 42
 
43 43
     this.bridgejid = bridgejid;
44 44
     this.peers = [];
@@ -47,7 +47,6 @@ function ColibriFocus(connection, bridgejid) {
47 47
     // media types of the conference
48 48
     this.media = ['audio', 'video'];
49 49
 
50
-    this.sid = Math.random().toString(36).substr(2, 12);
51 50
     this.connection.jingle.sessions[this.sid] = this;
52 51
     this.mychannel = [];
53 52
     this.channels = [];
@@ -556,67 +555,86 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
556 555
 
557 556
 // tell everyone about a new participants a=ssrc lines (isadd is true)
558 557
 // or a leaving participants a=ssrc lines
559
-// FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid
560
-ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
558
+ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd) {
561 559
     var self = this;
562 560
     this.peers.forEach(function (peerjid) {
563
-        if (peerjid == jid) return;
564
-        console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', jid);
561
+        if (peerjid == fromJid) return;
562
+        console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', fromJid);
565 563
         if (!self.remotessrc[peerjid]) {
566 564
             // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
567 565
             // possibly, this.remoteSSRC[session.peerjid] does not exist yet
568 566
             console.warn('do we really want to bother', peerjid, 'with updates yet?');
569 567
         }
570
-        var channel;
571 568
         var peersess = self.connection.jingle.jid2session[peerjid];
572
-        var modify = $iq({to: peerjid, type: 'set'})
573
-            .c('jingle', {
574
-                xmlns: 'urn:xmpp:jingle:1',
575
-                action: isadd ? 'addsource' : 'removesource',
576
-                initiator: peersess.initiator,
577
-                sid: peersess.sid
578
-            }
579
-        );
580
-        // FIXME: only announce video ssrcs since we mix audio and dont need
581
-        //      the audio ssrcs therefore
582
-        var modified = false;
583
-        for (channel = 0; channel < sdp.media.length; channel++) {
584
-            modified = true;
585
-            tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:');
586
-            modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))});
587
-            modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
588
-            // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
589
-            tmp.forEach(function (line) {
590
-                var idx = line.indexOf(' ');
591
-                var linessrc = line.substr(0, idx).substr(7);
592
-                modify.attrs({ssrc: linessrc});
593
-
594
-                var kv = line.substr(idx + 1);
595
-                modify.c('parameter');
596
-                if (kv.indexOf(':') == -1) {
597
-                    modify.attrs({ name: kv });
598
-                } else {
599
-                    modify.attrs({ name: kv.split(':', 2)[0] });
600
-                    modify.attrs({ value: kv.split(':', 2)[1] });
601
-                }
602
-                modify.up();
603
-            });
604
-            modify.up(); // end of source
605
-            modify.up(); // end of content
569
+        if(!peersess){
570
+            console.warn('no session with peer: '+peerjid+' yet...');
571
+            return;
606 572
         }
607
-        if (modified) {
608
-            self.connection.sendIQ(modify,
609
-                function (res) {
610
-                    console.warn('got modify result');
611
-                },
612
-                function (err) {
613
-                    console.warn('got modify error', err);
614
-                }
615
-            );
573
+
574
+        self.sendSSRCUpdateIq(sdpMediaSsrcs, peersess.sid, peersess.initiator, peerjid, isadd);
575
+    });
576
+};
577
+
578
+/**
579
+ * Overrides SessionBase.addSource.
580
+ *
581
+ * @param elem proprietary 'add source' Jingle request(XML node).
582
+ * @param fromJid JID of the participant to whom new ssrcs belong.
583
+ */
584
+ColibriFocus.prototype.addSource = function (elem, fromJid) {
585
+
586
+    var self = this;
587
+    this.peerconnection.addSource(elem);
588
+
589
+    var peerSsrc = this.remotessrc[fromJid];
590
+    //console.log("On ADD", self.addssrc, peerSsrc);
591
+    this.peerconnection.addssrc.forEach(function(val, idx){
592
+        if(!peerSsrc[idx]){
593
+            // add ssrc
594
+            peerSsrc[idx] = val;
616 595
         } else {
617
-            console.log('modification not necessary');
596
+            if(peerSsrc[idx].indexOf(val) == -1){
597
+                peerSsrc[idx] = peerSsrc[idx]+val;
598
+            }
599
+        }
600
+    });
601
+
602
+    var oldRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
603
+    this.modifySources(function(){
604
+        // Notify other participants about added ssrc
605
+        var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
606
+        var newSSRCs = oldRemoteSdp.getNewMedia(remoteSDP);
607
+        self.sendSSRCUpdate(newSSRCs, fromJid, true);
608
+    });
609
+};
610
+
611
+/**
612
+ * Overrides SessionBase.removeSource.
613
+ *
614
+ * @param elem proprietary 'remove source' Jingle request(XML node).
615
+ * @param fromJid JID of the participant to whom removed ssrcs belong.
616
+ */
617
+ColibriFocus.prototype.removeSource = function (elem, fromJid) {
618
+
619
+    var self = this;
620
+    this.peerconnection.removeSource(elem);
621
+
622
+    var peerSsrc = this.remotessrc[fromJid];
623
+    //console.log("On REMOVE", self.removessrc, peerSsrc);
624
+    this.peerconnection.removessrc.forEach(function(val, idx){
625
+        if(peerSsrc[idx]){
626
+            // Remove ssrc
627
+            peerSsrc[idx] = peerSsrc[idx].replace(val, '');
618 628
         }
619 629
     });
630
+
631
+    var oldSDP = new SDP(self.peerconnection.remoteDescription.sdp);
632
+    this.modifySources(function(){
633
+        // Notify other participants about removed ssrc
634
+        var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
635
+        var removedSSRCs = remoteSDP.getNewMedia(oldSDP);
636
+        self.sendSSRCUpdate(removedSSRCs, fromJid, false);
637
+    });
620 638
 };
621 639
 
622 640
 ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
@@ -630,7 +648,7 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
630 648
     this.updateChannel(remoteSDP, participant);
631 649
 
632 650
     // ACT 2: tell anyone else about the new SSRCs
633
-    this.sendSSRCUpdate(remoteSDP, session.peerjid, true);
651
+    this.sendSSRCUpdate(remoteSDP.getMediaSsrcMap(), session.peerjid, true);
634 652
 
635 653
     // ACT 3: note the SSRCs
636 654
     this.remotessrc[session.peerjid] = [];
@@ -792,7 +810,7 @@ ColibriFocus.prototype.terminate = function (session, reason) {
792 810
         sdp.media[j] += ssrcs[j];
793 811
         this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]);
794 812
     }
795
-    this.sendSSRCUpdate(sdp, session.peerjid, false);
813
+    this.sendSSRCUpdate(sdp.getMediaSsrcMap(), session.peerjid, false);
796 814
 
797 815
     delete this.remotessrc[session.peerjid];
798 816
     this.modifySources();

+ 8
- 0
libs/colibri/colibri.session.js Visa fil

@@ -61,6 +61,14 @@ ColibriSession.prototype.accept = function () {
61 61
     console.log('ColibriSession.accept');
62 62
 };
63 63
 
64
+ColibriSession.prototype.addSource = function (elem, fromJid) {
65
+    this.colibri.addSource(elem, fromJid);
66
+};
67
+
68
+ColibriSession.prototype.removeSource = function (elem, fromJid) {
69
+    this.colibri.removeSource(elem, fromJid);
70
+};
71
+
64 72
 ColibriSession.prototype.terminate = function (reason) {
65 73
     this.colibri.terminate(this, reason);
66 74
 };

+ 32
- 3
libs/strophe/strophe.jingle.adapter.js Visa fil

@@ -25,6 +25,12 @@ function TraceablePeerConnection(ice_config, constraints) {
25 25
      */
26 26
     this.pendingop = null;
27 27
 
28
+    /**
29
+     * Flag indicates that peer connection stream have changed and modifySources should proceed.
30
+     * @type {boolean}
31
+     */
32
+    this.switchstreams = false;
33
+
28 34
     // override as desired
29 35
     this.trace = function(what, info) {
30 36
         //console.warn('WTRACE', what, info);
@@ -197,6 +203,7 @@ TraceablePeerConnection.prototype.addSource = function (elem) {
197 203
     console.log('addssrc', new Date().getTime());
198 204
     console.log('ice', this.iceConnectionState);
199 205
     var sdp = new SDP(this.remoteDescription.sdp);
206
+    var mySdp = new SDP(this.peerconnection.localDescription.sdp);
200 207
 
201 208
     var self = this;
202 209
     $(elem).each(function (idx, content) {
@@ -205,6 +212,15 @@ TraceablePeerConnection.prototype.addSource = function (elem) {
205 212
         tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
206 213
         tmp.each(function () {
207 214
             var ssrc = $(this).attr('ssrc');
215
+            if(mySdp.containsSSRC(ssrc)){
216
+                /**
217
+                 * This happens when multiple participants change their streams at the same time and
218
+                 * ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
219
+                 * addssrc are scheduled for update IQ. See
220
+                 */
221
+                console.warn("Got add stream request for my own ssrc: "+ssrc);
222
+                return;
223
+            }
208 224
             $(this).find('>parameter').each(function () {
209 225
                 lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
210 226
                 if ($(this).attr('value') && $(this).attr('value').length)
@@ -233,6 +249,7 @@ TraceablePeerConnection.prototype.removeSource = function (elem) {
233 249
     console.log('removessrc', new Date().getTime());
234 250
     console.log('ice', this.iceConnectionState);
235 251
     var sdp = new SDP(this.remoteDescription.sdp);
252
+    var mySdp = new SDP(this.peerconnection.localDescription.sdp);
236 253
 
237 254
     var self = this;
238 255
     $(elem).each(function (idx, content) {
@@ -241,6 +258,11 @@ TraceablePeerConnection.prototype.removeSource = function (elem) {
241 258
         tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
242 259
         tmp.each(function () {
243 260
             var ssrc = $(this).attr('ssrc');
261
+            // This should never happen, but can be useful for bug detection
262
+            if(mySdp.containsSSRC(ssrc)){
263
+                console.error("Got remove stream request for my own ssrc: "+ssrc);
264
+                return;
265
+            }
244 266
             $(this).find('>parameter').each(function () {
245 267
                 lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
246 268
                 if ($(this).attr('value') && $(this).attr('value').length)
@@ -261,7 +283,8 @@ TraceablePeerConnection.prototype.removeSource = function (elem) {
261 283
 TraceablePeerConnection.prototype.modifySources = function(successCallback) {
262 284
     var self = this;
263 285
     if (this.signalingState == 'closed') return;
264
-    if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)){
286
+    if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
287
+        // There is nothing to do since scheduled job might have been executed by another succeeding call
265 288
         if(successCallback){
266 289
             successCallback();
267 290
         }
@@ -282,6 +305,9 @@ TraceablePeerConnection.prototype.modifySources = function(successCallback) {
282 305
         return;
283 306
     }
284 307
 
308
+    // Reset switch streams flag
309
+    this.switchstreams = false;
310
+
285 311
     var sdp = new SDP(this.remoteDescription.sdp);
286 312
 
287 313
     // add sources
@@ -486,8 +512,11 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
486 512
     }
487 513
     if (um.indexOf('screen') >= 0) {
488 514
         constraints.video = {
489
-            "mandatory": {
490
-                "chromeMediaSource": "screen"
515
+            mandatory: {
516
+                chromeMediaSource: "screen",
517
+                maxWidth: window.screen.width,
518
+                maxHeight: window.screen.height,
519
+                maxFrameRate: 3
491 520
             }
492 521
         };
493 522
     }

+ 2
- 2
libs/strophe/strophe.jingle.js Visa fil

@@ -141,10 +141,10 @@ Strophe.addConnectionPlugin('jingle', {
141 141
                 }
142 142
                 break;
143 143
             case 'addsource': // FIXME: proprietary
144
-                sess.addSource($(iq).find('>jingle>content'));
144
+                sess.addSource($(iq).find('>jingle>content'), fromJid);
145 145
                 break;
146 146
             case 'removesource': // FIXME: proprietary
147
-                sess.removeSource($(iq).find('>jingle>content'));
147
+                sess.removeSource($(iq).find('>jingle>content'), fromJid);
148 148
                 break;
149 149
             default:
150 150
                 console.warn('jingle action not implemented', action);

+ 70
- 315
libs/strophe/strophe.jingle.sdp.js Visa fil

@@ -11,7 +11,75 @@ function SDP(sdp) {
11 11
     this.session = this.media.shift() + '\r\n';
12 12
     this.raw = this.session + this.media.join('');
13 13
 }
14
-
14
+/**
15
+ * Returns map of MediaChannel mapped per channel idx.
16
+ */
17
+SDP.prototype.getMediaSsrcMap = function() {
18
+    var self = this;
19
+    var media_ssrcs = {};
20
+    for (channelNum = 0; channelNum < self.media.length; channelNum++) {
21
+        modified = true;
22
+        tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc:');
23
+        var type = SDPUtil.parse_mid(SDPUtil.find_line(self.media[channelNum], 'a=mid:'));
24
+        var channel = new MediaChannel(channelNum, type);
25
+        media_ssrcs[channelNum] = channel;
26
+        tmp.forEach(function (line) {
27
+            var linessrc = line.substring(7).split(' ')[0];
28
+            // allocate new ChannelSsrc
29
+            if(!channel.ssrcs[linessrc]) {
30
+                channel.ssrcs[linessrc] = new ChannelSsrc(linessrc, type);
31
+            }
32
+            channel.ssrcs[linessrc].lines.push(line);
33
+        });
34
+    }
35
+    return media_ssrcs;
36
+}
37
+/**
38
+ * Returns <tt>true</tt> if this SDP contains given SSRC.
39
+ * @param ssrc the ssrc to check.
40
+ * @returns {boolean} <tt>true</tt> if this SDP contains given SSRC.
41
+ */
42
+SDP.prototype.containsSSRC = function(ssrc) {
43
+    var channels = this.getMediaSsrcMap();
44
+    var contains = false;
45
+    Object.keys(channels).forEach(function(chNumber){
46
+        var channel = channels[chNumber];
47
+        //console.log("Check", channel, ssrc);
48
+        if(Object.keys(channel.ssrcs).indexOf(ssrc) != -1){
49
+            contains = true;
50
+        }
51
+    });
52
+    return contains;
53
+}
54
+/**
55
+ * Returns map of MediaChannel that contains only media not contained in <tt>otherSdp</tt>. Mapped by channel idx.
56
+ * @param otherSdp the other SDP to check ssrc with.
57
+ */
58
+SDP.prototype.getNewMedia = function(otherSdp) {
59
+    var myMedia = this.getMediaSsrcMap();
60
+    var othersMedia = otherSdp.getMediaSsrcMap();
61
+    var newMedia = {};
62
+    Object.keys(othersMedia).forEach(function(channelNum) {
63
+        var myChannel = myMedia[channelNum];
64
+        var othersChannel = othersMedia[channelNum];
65
+        if(!myChannel && othersChannel) {
66
+            // Add whole channel
67
+            newMedia[channelNum] = othersChannel;
68
+            return;
69
+        }
70
+        // Look for new ssrcs accross the channel
71
+        Object.keys(othersChannel.ssrcs).forEach(function(ssrc) {
72
+            if(Object.keys(myChannel.ssrcs).indexOf(ssrc) === -1) {
73
+                // Allocate channel if we've found ssrc that doesn't exist in our channel
74
+                if(!newMedia[channelNum]){
75
+                    newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
76
+                }
77
+                newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc];
78
+            }
79
+        })
80
+    });
81
+    return newMedia;
82
+}
15 83
 // remove iSAC and CN from SDP
16 84
 SDP.prototype.mangle = function () {
17 85
     var i, j, mline, lines, rtpmap, newdesc;
@@ -485,317 +553,4 @@ SDP.prototype.jingle2media = function (content) {
485 553
         }
486 554
     }
487 555
     return media;
488
-};
489
-
490
-SDPUtil = {
491
-    iceparams: function (mediadesc, sessiondesc) {
492
-        var data = null;
493
-        if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
494
-            SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
495
-            data = {
496
-                ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
497
-                pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
498
-            };
499
-        }
500
-        return data;
501
-    },
502
-    parse_iceufrag: function (line) {
503
-        return line.substring(12);
504
-    },
505
-    build_iceufrag: function (frag) {
506
-        return 'a=ice-ufrag:' + frag;
507
-    },
508
-    parse_icepwd: function (line) {
509
-        return line.substring(10);
510
-    },
511
-    build_icepwd: function (pwd) {
512
-        return 'a=ice-pwd:' + pwd;
513
-    },
514
-    parse_mid: function (line) {
515
-        return line.substring(6);
516
-    },
517
-    parse_mline: function (line) {
518
-        var parts = line.substring(2).split(' '),
519
-            data = {};
520
-        data.media = parts.shift();
521
-        data.port = parts.shift();
522
-        data.proto = parts.shift();
523
-        if (parts[parts.length - 1] === '') { // trailing whitespace
524
-            parts.pop();
525
-        }
526
-        data.fmt = parts;
527
-        return data;
528
-    },
529
-    build_mline: function (mline) {
530
-        return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
531
-    },
532
-    parse_rtpmap: function (line) {
533
-        var parts = line.substring(9).split(' '),
534
-            data = {};
535
-        data.id = parts.shift();
536
-        parts = parts[0].split('/');
537
-        data.name = parts.shift();
538
-        data.clockrate = parts.shift();
539
-        data.channels = parts.length ? parts.shift() : '1';
540
-        return data;
541
-    },
542
-    build_rtpmap: function (el) {
543
-        var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
544
-        if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
545
-            line += '/' + el.getAttribute('channels');
546
-        }
547
-        return line;
548
-    },
549
-    parse_crypto: function (line) {
550
-        var parts = line.substring(9).split(' '),
551
-            data = {};
552
-        data.tag = parts.shift();
553
-        data['crypto-suite'] = parts.shift();
554
-        data['key-params'] = parts.shift();
555
-        if (parts.length) {
556
-            data['session-params'] = parts.join(' ');
557
-        }
558
-        return data;
559
-    },
560
-    parse_fingerprint: function (line) { // RFC 4572
561
-        var parts = line.substring(14).split(' '),
562
-            data = {};
563
-        data.hash = parts.shift();
564
-        data.fingerprint = parts.shift();
565
-        // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
566
-        return data;
567
-    },
568
-    parse_fmtp: function (line) {
569
-        var parts = line.split(' '),
570
-            i, key, value,
571
-            data = [];
572
-        parts.shift();
573
-        parts = parts.join(' ').split(';');
574
-        for (i = 0; i < parts.length; i++) {
575
-            key = parts[i].split('=')[0];
576
-            while (key.length && key[0] == ' ') {
577
-                key = key.substring(1);
578
-            }
579
-            value = parts[i].split('=')[1];
580
-            if (key && value) {
581
-                data.push({name: key, value: value});
582
-            } else if (key) {
583
-                // rfc 4733 (DTMF) style stuff
584
-                data.push({name: '', value: key});
585
-            }
586
-        }
587
-        return data;
588
-    },
589
-    parse_icecandidate: function (line) {
590
-        var candidate = {},
591
-            elems = line.split(' ');
592
-        candidate.foundation = elems[0].substring(12);
593
-        candidate.component = elems[1];
594
-        candidate.protocol = elems[2].toLowerCase();
595
-        candidate.priority = elems[3];
596
-        candidate.ip = elems[4];
597
-        candidate.port = elems[5];
598
-        // elems[6] => "typ"
599
-        candidate.type = elems[7];
600
-        candidate.generation = 0; // default value, may be overwritten below
601
-        for (var i = 8; i < elems.length; i += 2) {
602
-            switch (elems[i]) {
603
-                case 'raddr':
604
-                    candidate['rel-addr'] = elems[i + 1];
605
-                    break;
606
-                case 'rport':
607
-                    candidate['rel-port'] = elems[i + 1];
608
-                    break;
609
-                case 'generation':
610
-                    candidate.generation = elems[i + 1];
611
-                    break;
612
-                default: // TODO
613
-                    console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
614
-            }
615
-        }
616
-        candidate.network = '1';
617
-        candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
618
-        return candidate;
619
-    },
620
-    build_icecandidate: function (cand) {
621
-        var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
622
-        line += ' ';
623
-        switch (cand.type) {
624
-            case 'srflx':
625
-            case 'prflx':
626
-            case 'relay':
627
-                if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
628
-                    line += 'raddr';
629
-                    line += ' ';
630
-                    line += cand['rel-addr'];
631
-                    line += ' ';
632
-                    line += 'rport';
633
-                    line += ' ';
634
-                    line += cand['rel-port'];
635
-                    line += ' ';
636
-                }
637
-                break;
638
-        }
639
-        line += 'generation';
640
-        line += ' ';
641
-        line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
642
-        return line;
643
-    },
644
-    parse_ssrc: function (desc) {
645
-        // proprietary mapping of a=ssrc lines
646
-        // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
647
-        // and parse according to that
648
-        var lines = desc.split('\r\n'),
649
-            data = {};
650
-        for (var i = 0; i < lines.length; i++) {
651
-            if (lines[i].substring(0, 7) == 'a=ssrc:') {
652
-                var idx = lines[i].indexOf(' ');
653
-                data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
654
-            }
655
-        }
656
-        return data;
657
-    },
658
-    parse_rtcpfb: function (line) {
659
-        var parts = line.substr(10).split(' ');
660
-        var data = {};
661
-        data.pt = parts.shift();
662
-        data.type = parts.shift();
663
-        data.params = parts;
664
-        return data;
665
-    },
666
-    parse_extmap: function (line) {
667
-        var parts = line.substr(9).split(' ');
668
-        var data = {};
669
-        data.value = parts.shift();
670
-        if (data.value.indexOf('/') != -1) {
671
-            data.direction = data.value.substr(data.value.indexOf('/') + 1);
672
-            data.value = data.value.substr(0, data.value.indexOf('/'));
673
-        } else {
674
-            data.direction = 'both';
675
-        }
676
-        data.uri = parts.shift();
677
-        data.params = parts;
678
-        return data;
679
-    },
680
-    find_line: function (haystack, needle, sessionpart) {
681
-        var lines = haystack.split('\r\n');
682
-        for (var i = 0; i < lines.length; i++) {
683
-            if (lines[i].substring(0, needle.length) == needle) {
684
-                return lines[i];
685
-            }
686
-        }
687
-        if (!sessionpart) {
688
-            return false;
689
-        }
690
-        // search session part
691
-        lines = sessionpart.split('\r\n');
692
-        for (var j = 0; j < lines.length; j++) {
693
-            if (lines[j].substring(0, needle.length) == needle) {
694
-                return lines[j];
695
-            }
696
-        }
697
-        return false;
698
-    },
699
-    find_lines: function (haystack, needle, sessionpart) {
700
-        var lines = haystack.split('\r\n'),
701
-            needles = [];
702
-        for (var i = 0; i < lines.length; i++) {
703
-            if (lines[i].substring(0, needle.length) == needle)
704
-                needles.push(lines[i]);
705
-        }
706
-        if (needles.length || !sessionpart) {
707
-            return needles;
708
-        }
709
-        // search session part
710
-        lines = sessionpart.split('\r\n');
711
-        for (var j = 0; j < lines.length; j++) {
712
-            if (lines[j].substring(0, needle.length) == needle) {
713
-                needles.push(lines[j]);
714
-            }
715
-        }
716
-        return needles;
717
-    },
718
-    candidateToJingle: function (line) {
719
-        // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
720
-        //      <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
721
-        if (line.substring(0, 12) != 'a=candidate:') {
722
-            console.log('parseCandidate called with a line that is not a candidate line');
723
-            console.log(line);
724
-            return null;
725
-        }
726
-        if (line.substring(line.length - 2) == '\r\n') // chomp it
727
-            line = line.substring(0, line.length - 2);
728
-        var candidate = {},
729
-            elems = line.split(' '),
730
-            i;
731
-        if (elems[6] != 'typ') {
732
-            console.log('did not find typ in the right place');
733
-            console.log(line);
734
-            return null;
735
-        }
736
-        candidate.foundation = elems[0].substring(12);
737
-        candidate.component = elems[1];
738
-        candidate.protocol = elems[2].toLowerCase();
739
-        candidate.priority = elems[3];
740
-        candidate.ip = elems[4];
741
-        candidate.port = elems[5];
742
-        // elems[6] => "typ"
743
-        candidate.type = elems[7];
744
-        for (i = 8; i < elems.length; i += 2) {
745
-            switch (elems[i]) {
746
-                case 'raddr':
747
-                    candidate['rel-addr'] = elems[i + 1];
748
-                    break;
749
-                case 'rport':
750
-                    candidate['rel-port'] = elems[i + 1];
751
-                    break;
752
-                case 'generation':
753
-                    candidate.generation = elems[i + 1];
754
-                    break;
755
-                default: // TODO
756
-                    console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
757
-            }
758
-        }
759
-        candidate.network = '1';
760
-        candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
761
-        return candidate;
762
-    },
763
-    candidateFromJingle: function (cand) {
764
-        var line = 'a=candidate:';
765
-        line += cand.getAttribute('foundation');
766
-        line += ' ';
767
-        line += cand.getAttribute('component');
768
-        line += ' ';
769
-        line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
770
-        line += ' ';
771
-        line += cand.getAttribute('priority');
772
-        line += ' ';
773
-        line += cand.getAttribute('ip');
774
-        line += ' ';
775
-        line += cand.getAttribute('port');
776
-        line += ' ';
777
-        line += 'typ';
778
-        line += ' ' + cand.getAttribute('type');
779
-        line += ' ';
780
-        switch (cand.getAttribute('type')) {
781
-            case 'srflx':
782
-            case 'prflx':
783
-            case 'relay':
784
-                if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
785
-                    line += 'raddr';
786
-                    line += ' ';
787
-                    line += cand.getAttribute('rel-addr');
788
-                    line += ' ';
789
-                    line += 'rport';
790
-                    line += ' ';
791
-                    line += cand.getAttribute('rel-port');
792
-                    line += ' ';
793
-                }
794
-                break;
795
-        }
796
-        line += 'generation';
797
-        line += ' ';
798
-        line += cand.getAttribute('generation') || '0';
799
-        return line + '\r\n';
800
-    }
801
-};
556
+};

+ 353
- 0
libs/strophe/strophe.jingle.sdp.util.js Visa fil

@@ -0,0 +1,353 @@
1
+/**
2
+ * Contains utility classes used in SDP class.
3
+ *
4
+ */
5
+
6
+/**
7
+ * Class holds a=ssrc lines and media type a=mid
8
+ * @param ssrc synchronization source identifier number(a=ssrc lines from SDP)
9
+ * @param type media type eg. "audio" or "video"(a=mid frm SDP)
10
+ * @constructor
11
+ */
12
+function ChannelSsrc(ssrc, type) {
13
+    this.ssrc = ssrc;
14
+    this.type = type;
15
+    this.lines = [];
16
+}
17
+
18
+/**
19
+ * Helper class represents media channel. Is a container for ChannelSsrc, holds channel idx and media type.
20
+ * @param channelNumber channel idx in SDP media array.
21
+ * @param mediaType media type(a=mid)
22
+ * @constructor
23
+ */
24
+function MediaChannel(channelNumber, mediaType) {
25
+    /**
26
+     * SDP channel number
27
+     * @type {*}
28
+     */
29
+    this.chNumber = channelNumber;
30
+    /**
31
+     * Channel media type(a=mid)
32
+     * @type {*}
33
+     */
34
+    this.mediaType = mediaType;
35
+    /**
36
+     * The maps of ssrc numbers to ChannelSsrc objects.
37
+     */
38
+    this.ssrcs = {};
39
+}
40
+
41
+SDPUtil = {
42
+    iceparams: function (mediadesc, sessiondesc) {
43
+        var data = null;
44
+        if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
45
+            SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
46
+            data = {
47
+                ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
48
+                pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
49
+            };
50
+        }
51
+        return data;
52
+    },
53
+    parse_iceufrag: function (line) {
54
+        return line.substring(12);
55
+    },
56
+    build_iceufrag: function (frag) {
57
+        return 'a=ice-ufrag:' + frag;
58
+    },
59
+    parse_icepwd: function (line) {
60
+        return line.substring(10);
61
+    },
62
+    build_icepwd: function (pwd) {
63
+        return 'a=ice-pwd:' + pwd;
64
+    },
65
+    parse_mid: function (line) {
66
+        return line.substring(6);
67
+    },
68
+    parse_mline: function (line) {
69
+        var parts = line.substring(2).split(' '),
70
+            data = {};
71
+        data.media = parts.shift();
72
+        data.port = parts.shift();
73
+        data.proto = parts.shift();
74
+        if (parts[parts.length - 1] === '') { // trailing whitespace
75
+            parts.pop();
76
+        }
77
+        data.fmt = parts;
78
+        return data;
79
+    },
80
+    build_mline: function (mline) {
81
+        return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
82
+    },
83
+    parse_rtpmap: function (line) {
84
+        var parts = line.substring(9).split(' '),
85
+            data = {};
86
+        data.id = parts.shift();
87
+        parts = parts[0].split('/');
88
+        data.name = parts.shift();
89
+        data.clockrate = parts.shift();
90
+        data.channels = parts.length ? parts.shift() : '1';
91
+        return data;
92
+    },
93
+    build_rtpmap: function (el) {
94
+        var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
95
+        if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
96
+            line += '/' + el.getAttribute('channels');
97
+        }
98
+        return line;
99
+    },
100
+    parse_crypto: function (line) {
101
+        var parts = line.substring(9).split(' '),
102
+            data = {};
103
+        data.tag = parts.shift();
104
+        data['crypto-suite'] = parts.shift();
105
+        data['key-params'] = parts.shift();
106
+        if (parts.length) {
107
+            data['session-params'] = parts.join(' ');
108
+        }
109
+        return data;
110
+    },
111
+    parse_fingerprint: function (line) { // RFC 4572
112
+        var parts = line.substring(14).split(' '),
113
+            data = {};
114
+        data.hash = parts.shift();
115
+        data.fingerprint = parts.shift();
116
+        // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
117
+        return data;
118
+    },
119
+    parse_fmtp: function (line) {
120
+        var parts = line.split(' '),
121
+            i, key, value,
122
+            data = [];
123
+        parts.shift();
124
+        parts = parts.join(' ').split(';');
125
+        for (i = 0; i < parts.length; i++) {
126
+            key = parts[i].split('=')[0];
127
+            while (key.length && key[0] == ' ') {
128
+                key = key.substring(1);
129
+            }
130
+            value = parts[i].split('=')[1];
131
+            if (key && value) {
132
+                data.push({name: key, value: value});
133
+            } else if (key) {
134
+                // rfc 4733 (DTMF) style stuff
135
+                data.push({name: '', value: key});
136
+            }
137
+        }
138
+        return data;
139
+    },
140
+    parse_icecandidate: function (line) {
141
+        var candidate = {},
142
+            elems = line.split(' ');
143
+        candidate.foundation = elems[0].substring(12);
144
+        candidate.component = elems[1];
145
+        candidate.protocol = elems[2].toLowerCase();
146
+        candidate.priority = elems[3];
147
+        candidate.ip = elems[4];
148
+        candidate.port = elems[5];
149
+        // elems[6] => "typ"
150
+        candidate.type = elems[7];
151
+        candidate.generation = 0; // default value, may be overwritten below
152
+        for (var i = 8; i < elems.length; i += 2) {
153
+            switch (elems[i]) {
154
+                case 'raddr':
155
+                    candidate['rel-addr'] = elems[i + 1];
156
+                    break;
157
+                case 'rport':
158
+                    candidate['rel-port'] = elems[i + 1];
159
+                    break;
160
+                case 'generation':
161
+                    candidate.generation = elems[i + 1];
162
+                    break;
163
+                default: // TODO
164
+                    console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
165
+            }
166
+        }
167
+        candidate.network = '1';
168
+        candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
169
+        return candidate;
170
+    },
171
+    build_icecandidate: function (cand) {
172
+        var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
173
+        line += ' ';
174
+        switch (cand.type) {
175
+            case 'srflx':
176
+            case 'prflx':
177
+            case 'relay':
178
+                if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
179
+                    line += 'raddr';
180
+                    line += ' ';
181
+                    line += cand['rel-addr'];
182
+                    line += ' ';
183
+                    line += 'rport';
184
+                    line += ' ';
185
+                    line += cand['rel-port'];
186
+                    line += ' ';
187
+                }
188
+                break;
189
+        }
190
+        line += 'generation';
191
+        line += ' ';
192
+        line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
193
+        return line;
194
+    },
195
+    parse_ssrc: function (desc) {
196
+        // proprietary mapping of a=ssrc lines
197
+        // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
198
+        // and parse according to that
199
+        var lines = desc.split('\r\n'),
200
+            data = {};
201
+        for (var i = 0; i < lines.length; i++) {
202
+            if (lines[i].substring(0, 7) == 'a=ssrc:') {
203
+                var idx = lines[i].indexOf(' ');
204
+                data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
205
+            }
206
+        }
207
+        return data;
208
+    },
209
+    parse_rtcpfb: function (line) {
210
+        var parts = line.substr(10).split(' ');
211
+        var data = {};
212
+        data.pt = parts.shift();
213
+        data.type = parts.shift();
214
+        data.params = parts;
215
+        return data;
216
+    },
217
+    parse_extmap: function (line) {
218
+        var parts = line.substr(9).split(' ');
219
+        var data = {};
220
+        data.value = parts.shift();
221
+        if (data.value.indexOf('/') != -1) {
222
+            data.direction = data.value.substr(data.value.indexOf('/') + 1);
223
+            data.value = data.value.substr(0, data.value.indexOf('/'));
224
+        } else {
225
+            data.direction = 'both';
226
+        }
227
+        data.uri = parts.shift();
228
+        data.params = parts;
229
+        return data;
230
+    },
231
+    find_line: function (haystack, needle, sessionpart) {
232
+        var lines = haystack.split('\r\n');
233
+        for (var i = 0; i < lines.length; i++) {
234
+            if (lines[i].substring(0, needle.length) == needle) {
235
+                return lines[i];
236
+            }
237
+        }
238
+        if (!sessionpart) {
239
+            return false;
240
+        }
241
+        // search session part
242
+        lines = sessionpart.split('\r\n');
243
+        for (var j = 0; j < lines.length; j++) {
244
+            if (lines[j].substring(0, needle.length) == needle) {
245
+                return lines[j];
246
+            }
247
+        }
248
+        return false;
249
+    },
250
+    find_lines: function (haystack, needle, sessionpart) {
251
+        var lines = haystack.split('\r\n'),
252
+            needles = [];
253
+        for (var i = 0; i < lines.length; i++) {
254
+            if (lines[i].substring(0, needle.length) == needle)
255
+                needles.push(lines[i]);
256
+        }
257
+        if (needles.length || !sessionpart) {
258
+            return needles;
259
+        }
260
+        // search session part
261
+        lines = sessionpart.split('\r\n');
262
+        for (var j = 0; j < lines.length; j++) {
263
+            if (lines[j].substring(0, needle.length) == needle) {
264
+                needles.push(lines[j]);
265
+            }
266
+        }
267
+        return needles;
268
+    },
269
+    candidateToJingle: function (line) {
270
+        // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
271
+        //      <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
272
+        if (line.substring(0, 12) != 'a=candidate:') {
273
+            console.log('parseCandidate called with a line that is not a candidate line');
274
+            console.log(line);
275
+            return null;
276
+        }
277
+        if (line.substring(line.length - 2) == '\r\n') // chomp it
278
+            line = line.substring(0, line.length - 2);
279
+        var candidate = {},
280
+            elems = line.split(' '),
281
+            i;
282
+        if (elems[6] != 'typ') {
283
+            console.log('did not find typ in the right place');
284
+            console.log(line);
285
+            return null;
286
+        }
287
+        candidate.foundation = elems[0].substring(12);
288
+        candidate.component = elems[1];
289
+        candidate.protocol = elems[2].toLowerCase();
290
+        candidate.priority = elems[3];
291
+        candidate.ip = elems[4];
292
+        candidate.port = elems[5];
293
+        // elems[6] => "typ"
294
+        candidate.type = elems[7];
295
+        for (i = 8; i < elems.length; i += 2) {
296
+            switch (elems[i]) {
297
+                case 'raddr':
298
+                    candidate['rel-addr'] = elems[i + 1];
299
+                    break;
300
+                case 'rport':
301
+                    candidate['rel-port'] = elems[i + 1];
302
+                    break;
303
+                case 'generation':
304
+                    candidate.generation = elems[i + 1];
305
+                    break;
306
+                default: // TODO
307
+                    console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
308
+            }
309
+        }
310
+        candidate.network = '1';
311
+        candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
312
+        return candidate;
313
+    },
314
+    candidateFromJingle: function (cand) {
315
+        var line = 'a=candidate:';
316
+        line += cand.getAttribute('foundation');
317
+        line += ' ';
318
+        line += cand.getAttribute('component');
319
+        line += ' ';
320
+        line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
321
+        line += ' ';
322
+        line += cand.getAttribute('priority');
323
+        line += ' ';
324
+        line += cand.getAttribute('ip');
325
+        line += ' ';
326
+        line += cand.getAttribute('port');
327
+        line += ' ';
328
+        line += 'typ';
329
+        line += ' ' + cand.getAttribute('type');
330
+        line += ' ';
331
+        switch (cand.getAttribute('type')) {
332
+            case 'srflx':
333
+            case 'prflx':
334
+            case 'relay':
335
+                if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
336
+                    line += 'raddr';
337
+                    line += ' ';
338
+                    line += cand.getAttribute('rel-addr');
339
+                    line += ' ';
340
+                    line += 'rport';
341
+                    line += ' ';
342
+                    line += cand.getAttribute('rel-port');
343
+                    line += ' ';
344
+                }
345
+                break;
346
+        }
347
+        line += 'generation';
348
+        line += ' ';
349
+        line += cand.getAttribute('generation') || '0';
350
+        return line + '\r\n';
351
+    }
352
+};
353
+

+ 17
- 2
libs/strophe/strophe.jingle.session.js Visa fil

@@ -3,10 +3,9 @@
3 3
 JingleSession.prototype = Object.create(SessionBase.prototype);
4 4
 function JingleSession(me, sid, connection) {
5 5
 
6
-    SessionBase.call(this, connection);
6
+    SessionBase.call(this, connection, sid);
7 7
 
8 8
     this.me = me;
9
-    this.sid = sid;
10 9
     this.initiator = null;
11 10
     this.responder = null;
12 11
     this.isInitiator = null;
@@ -155,6 +154,22 @@ JingleSession.prototype.accept = function () {
155 154
     );
156 155
 };
157 156
 
157
+/**
158
+ * Implements SessionBase.sendSSRCUpdate.
159
+ */
160
+JingleSession.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isadd) {
161
+
162
+    var self = this;
163
+    console.log('tell', self.peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from' + self.me);
164
+
165
+    if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){
166
+        console.log("Too early to send updates");
167
+        return;
168
+    }
169
+
170
+    this.sendSSRCUpdateIq(sdpMediaSsrcs, self.sid, self.initiator, self.peerjid, isadd);
171
+};
172
+
158 173
 JingleSession.prototype.terminate = function (reason) {
159 174
     this.state = 'ended';
160 175
     this.reason = reason;

+ 138
- 4
libs/strophe/strophe.jingle.sessionbase.js Visa fil

@@ -1,11 +1,13 @@
1 1
 /**
2 2
  * Base class for ColibriFocus and JingleSession.
3 3
  * @param connection Strophe connection object
4
+ * @param sid my session identifier(resource)
4 5
  * @constructor
5 6
  */
6
-function SessionBase(connection){
7
+function SessionBase(connection, sid){
7 8
 
8 9
     this.connection = connection;
10
+    this.sid = sid;
9 11
     this.peerconnection
10 12
         = new TraceablePeerConnection(
11 13
             connection.jingle.ice_config,
@@ -13,26 +15,158 @@ function SessionBase(connection){
13 15
 }
14 16
 
15 17
 
16
-SessionBase.prototype.modifySources = function() {
18
+SessionBase.prototype.modifySources = function (successCallback) {
17 19
     var self = this;
18 20
     this.peerconnection.modifySources(function(){
19 21
         $(document).trigger('setLocalDescription.jingle', [self.sid]);
22
+        if(successCallback) {
23
+            successCallback();
24
+        }
20 25
     });
21 26
 };
22 27
 
23
-SessionBase.prototype.addSource = function (elem) {
28
+SessionBase.prototype.addSource = function (elem, fromJid) {
24 29
 
25 30
     this.peerconnection.addSource(elem);
26 31
 
27 32
     this.modifySources();
28 33
 };
29 34
 
30
-SessionBase.prototype.removeSource = function (elem) {
35
+SessionBase.prototype.removeSource = function (elem, fromJid) {
31 36
 
32 37
     this.peerconnection.removeSource(elem);
33 38
 
34 39
     this.modifySources();
35 40
 };
41
+/**
42
+ * Switches video streams.
43
+ * @param new_stream new stream that will be used as video of this session.
44
+ * @param oldStream old video stream of this session.
45
+ * @param success_callback callback executed after successful stream switch.
46
+ */
47
+SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
48
+
49
+    var self = this;
50
+
51
+    // Remember SDP to figure out added/removed SSRCs
52
+    var oldSdp = new SDP(self.peerconnection.localDescription.sdp);
53
+
54
+    self.peerconnection.removeStream(oldStream);
55
+
56
+    self.connection.jingle.localVideo = new_stream;
57
+    self.peerconnection.addStream(self.connection.jingle.localVideo);
58
+
59
+    self.connection.jingle.localStreams = [];
60
+    self.connection.jingle.localStreams.push(self.connection.jingle.localAudio);
61
+    self.connection.jingle.localStreams.push(self.connection.jingle.localVideo);
62
+
63
+    self.peerconnection.switchstreams = true;
64
+    self.modifySources(function() {
65
+        console.log('modify sources done');
66
+
67
+        var newSdp = new SDP(self.peerconnection.localDescription.sdp);
68
+        console.log("SDPs", oldSdp, newSdp);
69
+        self.notifyMySSRCUpdate(oldSdp, newSdp);
70
+
71
+        success_callback();
72
+    });
73
+};
74
+
75
+/**
76
+ * Figures out added/removed ssrcs and send update IQs.
77
+ * @param old_sdp SDP object for old description.
78
+ * @param new_sdp SDP object for new description.
79
+ */
80
+SessionBase.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
81
+
82
+    var old_media = old_sdp.getMediaSsrcMap();
83
+    var new_media = new_sdp.getMediaSsrcMap();
84
+    //console.log("old/new medias: ", old_media, new_media);
85
+
86
+    var toAdd = old_sdp.getNewMedia(new_sdp);
87
+    var toRemove = new_sdp.getNewMedia(old_sdp);
88
+    //console.log("to add", toAdd);
89
+    //console.log("to remove", toRemove);
90
+    if(Object.keys(toRemove).length > 0){
91
+        this.sendSSRCUpdate(toRemove, null, false);
92
+    }
93
+    if(Object.keys(toAdd).length > 0){
94
+        this.sendSSRCUpdate(toAdd, null, true);
95
+    }
96
+};
97
+
98
+/**
99
+ * Empty method that does nothing by default. It should send SSRC update IQs to session participants.
100
+ * @param sdpMediaSsrcs array of
101
+ * @param fromJid
102
+ * @param isAdd
103
+ */
104
+SessionBase.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isAdd) {
105
+    //FIXME: put default implementation here(maybe from JingleSession?)
106
+}
107
+
108
+/**
109
+ * Sends SSRC update IQ.
110
+ * @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
111
+ * @param sid session identifier that will be put into the IQ.
112
+ * @param initiator initiator identifier.
113
+ * @param toJid destination Jid
114
+ * @param isAdd indicates if this is remove or add operation.
115
+ */
116
+SessionBase.prototype.sendSSRCUpdateIq = function(sdpMediaSsrcs, sid, initiator, toJid, isAdd) {
117
+
118
+    var self = this;
119
+    var modify = $iq({to: toJid, type: 'set'})
120
+        .c('jingle', {
121
+            xmlns: 'urn:xmpp:jingle:1',
122
+            action: isAdd ? 'addsource' : 'removesource',
123
+            initiator: initiator,
124
+            sid: sid
125
+        }
126
+    );
127
+    // FIXME: only announce video ssrcs since we mix audio and dont need
128
+    //      the audio ssrcs therefore
129
+    var modified = false;
130
+    Object.keys(sdpMediaSsrcs).forEach(function(channelNum){
131
+        modified = true;
132
+        var channel = sdpMediaSsrcs[channelNum];
133
+        modify.c('content', {name: channel.mediaType});
134
+        // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
135
+        // generate sources from lines
136
+        Object.keys(channel.ssrcs).forEach(function(ssrcNum) {
137
+            var mediaSsrc = channel.ssrcs[ssrcNum];
138
+            modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
139
+            modify.attrs({ssrc: mediaSsrc.ssrc});
140
+            // iterate over ssrc lines
141
+            mediaSsrc.lines.forEach(function (line) {
142
+                var idx = line.indexOf(' ');
143
+                var kv = line.substr(idx + 1);
144
+                modify.c('parameter');
145
+                if (kv.indexOf(':') == -1) {
146
+                    modify.attrs({ name: kv });
147
+                } else {
148
+                    modify.attrs({ name: kv.split(':', 2)[0] });
149
+                    modify.attrs({ value: kv.split(':', 2)[1] });
150
+                }
151
+                modify.up(); // end of parameter
152
+            });
153
+            modify.up(); // end of source
154
+        });
155
+        modify.up(); // end of content
156
+    });
157
+    if (modified) {
158
+        self.connection.sendIQ(modify,
159
+            function (res) {
160
+                console.info('got modify result', res);
161
+            },
162
+            function (err) {
163
+                console.error('got modify error', err);
164
+            }
165
+        );
166
+    } else {
167
+        console.log('modification not necessary');
168
+    }
169
+};
36 170
 
37 171
 // SDP-based mute by going recvonly/sendrecv
38 172
 // FIXME: should probably black out the screen as well

+ 8
- 0
muc.js Visa fil

@@ -232,6 +232,14 @@ Strophe.addConnectionPlugin('emuc', {
232 232
         this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
233 233
         this.presMap['source' + sourceNumber + '_direction'] = direction;
234 234
     },
235
+    clearPresenceMedia: function () {
236
+        var self = this;
237
+        Object.keys(this.presMap).forEach( function(key) {
238
+            if(key.indexOf('source') != -1) {
239
+                delete self.presMap[key];
240
+            }
241
+        });
242
+    },
235 243
     addPreziToPresence: function (url, currentSlide) {
236 244
         this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
237 245
         this.presMap['preziurl'] = url;

Laddar…
Avbryt
Spara