浏览代码

Splits strophe and colibri libs into separate scripts.

master
paweldomas 11 年前
父节点
当前提交
3c7de1a79d

+ 7
- 2
index.html 查看文件

@@ -2,8 +2,13 @@
2 2
   <head>
3 3
     <title>WebRTC, meet the Jitsi Videobridge</title>
4 4
     <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
5
-    <script src="libs/strophejingle.bundle.js?v=7"></script><!-- strophe.jingle bundle -->
6
-    <script src="libs/colibri.js?v=7"></script><!-- colibri focus implementation -->
5
+    <script src="libs/strophe/strophe.jingle.adapter.js?v=1"></script><!-- strophe.jingle bundles -->
6
+    <script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script>
7
+    <script src="libs/strophe/strophe.jingle.js?v=1"></script>
8
+    <script src="libs/strophe/strophe.jingle.sdp.js?v=1"></script>
9
+    <script src="libs/strophe/strophe.jingle.session.js?v=1"></script>
10
+    <script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
11
+    <script src="libs/colibri/colibri.session.js?v=1"></script>
7 12
     <script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
8 13
     <script src="muc.js?v=9"></script><!-- simple MUC library -->
9 14
     <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->

libs/colibri.js → libs/colibri/colibri.focus.js 查看文件

@@ -1,4 +1,4 @@
1
-/* colibri.js -- a COLIBRI focus 
1
+/* colibri.js -- a COLIBRI focus
2 2
  * The colibri spec has been submitted to the XMPP Standards Foundation
3 3
  * for publications as a XMPP extensions:
4 4
  * http://xmpp.org/extensions/inbox/colibri.html
@@ -7,32 +7,32 @@
7 7
  * in the conference. The conference itself can be ad-hoc, through a
8 8
  * MUC, through PubSub, etc.
9 9
  *
10
- * colibri.js relies heavily on the strophe.jingle library available 
10
+ * colibri.js relies heavily on the strophe.jingle library available
11 11
  * from https://github.com/ESTOS/strophe.jingle
12 12
  * and interoperates with the Jitsi videobridge available from
13 13
  * https://jitsi.org/Projects/JitsiVideobridge
14 14
  */
15 15
 /*
16
-Copyright (c) 2013 ESTOS GmbH
17
-
18
-Permission is hereby granted, free of charge, to any person obtaining a copy
19
-of this software and associated documentation files (the "Software"), to deal
20
-in the Software without restriction, including without limitation the rights
21
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
-copies of the Software, and to permit persons to whom the Software is
23
-furnished to do so, subject to the following conditions:
24
-
25
-The above copyright notice and this permission notice shall be included in
26
-all copies or substantial portions of the Software.
27
-
28
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34
-THE SOFTWARE.
35
-*/
16
+ Copyright (c) 2013 ESTOS GmbH
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights
21
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+ copies of the Software, and to permit persons to whom the Software is
23
+ furnished to do so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in
26
+ all copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34
+ THE SOFTWARE.
35
+ */
36 36
 /* jshint -W117 */
37 37
 function ColibriFocus(connection, bridgejid) {
38 38
     this.connection = connection;
@@ -86,20 +86,20 @@ ColibriFocus.prototype.makeConference = function (peers) {
86 86
     this.peerconnection.oniceconnectionstatechange = function (event) {
87 87
         console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
88 88
         /*
89
-        if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
90
-            console.log('adding new remote SSRCs from iceconnectionstatechange');
91
-            window.setTimeout(function() { self.modifySources(); }, 1000);
92
-        }
93
-        */
89
+         if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
90
+         console.log('adding new remote SSRCs from iceconnectionstatechange');
91
+         window.setTimeout(function() { self.modifySources(); }, 1000);
92
+         }
93
+         */
94 94
     };
95 95
     this.peerconnection.onsignalingstatechange = function (event) {
96 96
         console.warn(self.peerconnection.signalingState);
97 97
         /*
98
-        if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
99
-            console.log('adding new remote SSRCs from signalingstatechange');
100
-            window.setTimeout(function() { self.modifySources(); }, 1000);
101
-        }
102
-        */
98
+         if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
99
+         console.log('adding new remote SSRCs from signalingstatechange');
100
+         window.setTimeout(function() { self.modifySources(); }, 1000);
101
+         }
102
+         */
103 103
     };
104 104
     this.peerconnection.onaddstream = function (event) {
105 105
         self.remoteStream = event.stream;
@@ -163,7 +163,7 @@ ColibriFocus.prototype._makeConference = function () {
163 163
     /*
164 164
     var localSDP = new SDP(this.peerconnection.localDescription.sdp);
165 165
     localSDP.media.forEach(function (media, channel) {
166
-        var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; 
166
+        var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
167 167
         elem.c('content', {name: name});
168 168
         elem.c('channel', {initiator: 'false', expire: '15'});
169 169
 
@@ -303,10 +303,10 @@ ColibriFocus.prototype.createdConference = function (result) {
303 303
                             elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
304 304
                             var localSDP = new SDP(self.peerconnection.localDescription.sdp);
305 305
                             localSDP.media.forEach(function (media, channel) {
306
-                                var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; 
306
+                                var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
307 307
                                 elem.c('content', {name: name});
308 308
                                 elem.c('channel', {
309
-                                    initiator: 'true', 
309
+                                    initiator: 'true',
310 310
                                     expire: '15',
311 311
                                     id: self.mychannel[channel].attr('id')
312 312
                                 });
@@ -495,7 +495,7 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
495 495
     elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
496 496
     var localSDP = new SDP(this.peerconnection.localDescription.sdp);
497 497
     localSDP.media.forEach(function (media, channel) {
498
-        var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; 
498
+        var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
499 499
         elem.c('content', {name: name});
500 500
         elem.c('channel', {initiator: 'true', expire:'15'});
501 501
         elem.up(); // end of channel
@@ -531,7 +531,7 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
531 531
             // TODO: too much copy-paste
532 532
             var rtpmap = SDPUtil.parse_rtpmap(val);
533 533
             change.c('payload-type', rtpmap);
534
-            // 
534
+            //
535 535
             // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
536 536
             /*
537 537
             if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
@@ -582,7 +582,7 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
582 582
                 sid: peersess.sid
583 583
             }
584 584
         );
585
-        // FIXME: only announce video ssrcs since we mix audio and dont need 
585
+        // FIXME: only announce video ssrcs since we mix audio and dont need
586 586
         //      the audio ssrcs therefore
587 587
         var modified = false;
588 588
         for (channel = 0; channel < sdp.media.length; channel++) {
@@ -867,7 +867,7 @@ ColibriFocus.prototype.modifySources = function () {
867 867
                         self.pendingop = null;
868 868
                     }
869 869
 
870
-                    // FIXME: pushing down an answer while ice connection state 
870
+                    // FIXME: pushing down an answer while ice connection state
871 871
                     // is still checking is bad...
872 872
                     //console.log(self.peerconnection.iceConnectionState);
873 873
 
@@ -931,55 +931,3 @@ ColibriFocus.prototype.hardMuteVideo = function (muted) {
931 931
         track.enabled = !muted;
932 932
     });
933 933
 };
934
-
935
-// A colibri session is similar to a jingle session, it just implements some things differently
936
-// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
937
-function ColibriSession(me, sid, connection) {
938
-    this.me = me;
939
-    this.sid = sid;
940
-    this.connection = connection;
941
-    //this.peerconnection = null;
942
-    //this.mychannel = null;
943
-    //this.channels = null;
944
-    this.peerjid = null;
945
-
946
-    this.colibri = null;
947
-}
948
-
949
-// implementation of JingleSession interface
950
-ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
951
-    this.peerjid = peerjid;
952
-};
953
-
954
-ColibriSession.prototype.sendOffer = function (offer) {
955
-    console.log('ColibriSession.sendOffer');
956
-};
957
-
958
-
959
-ColibriSession.prototype.accept = function () {
960
-    console.log('ColibriSession.accept');
961
-};
962
-
963
-ColibriSession.prototype.terminate = function (reason) {
964
-    this.colibri.terminate(this, reason);
965
-};
966
-
967
-ColibriSession.prototype.active = function () {
968
-    console.log('ColibriSession.active');
969
-};
970
-
971
-ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
972
-    this.colibri.setRemoteDescription(this, elem, desctype);
973
-};
974
-
975
-ColibriSession.prototype.addIceCandidate = function (elem) {
976
-    this.colibri.addIceCandidate(this, elem);
977
-};
978
-
979
-ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
980
-    console.log('ColibriSession.sendAnswer');
981
-};
982
-
983
-ColibriSession.prototype.sendTerminate = function (reason, text) {
984
-    console.log('ColibriSession.sendTerminate');
985
-};

+ 86
- 0
libs/colibri/colibri.session.js 查看文件

@@ -0,0 +1,86 @@
1
+/* colibri.js -- a COLIBRI focus 
2
+ * The colibri spec has been submitted to the XMPP Standards Foundation
3
+ * for publications as a XMPP extensions:
4
+ * http://xmpp.org/extensions/inbox/colibri.html
5
+ *
6
+ * colibri.js is a participating focus, i.e. the focus participates
7
+ * in the conference. The conference itself can be ad-hoc, through a
8
+ * MUC, through PubSub, etc.
9
+ *
10
+ * colibri.js relies heavily on the strophe.jingle library available 
11
+ * from https://github.com/ESTOS/strophe.jingle
12
+ * and interoperates with the Jitsi videobridge available from
13
+ * https://jitsi.org/Projects/JitsiVideobridge
14
+ */
15
+/*
16
+Copyright (c) 2013 ESTOS GmbH
17
+
18
+Permission is hereby granted, free of charge, to any person obtaining a copy
19
+of this software and associated documentation files (the "Software"), to deal
20
+in the Software without restriction, including without limitation the rights
21
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+copies of the Software, and to permit persons to whom the Software is
23
+furnished to do so, subject to the following conditions:
24
+
25
+The above copyright notice and this permission notice shall be included in
26
+all copies or substantial portions of the Software.
27
+
28
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34
+THE SOFTWARE.
35
+*/
36
+// A colibri session is similar to a jingle session, it just implements some things differently
37
+// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
38
+function ColibriSession(me, sid, connection) {
39
+    this.me = me;
40
+    this.sid = sid;
41
+    this.connection = connection;
42
+    //this.peerconnection = null;
43
+    //this.mychannel = null;
44
+    //this.channels = null;
45
+    this.peerjid = null;
46
+
47
+    this.colibri = null;
48
+}
49
+
50
+// implementation of JingleSession interface
51
+ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
52
+    this.peerjid = peerjid;
53
+};
54
+
55
+ColibriSession.prototype.sendOffer = function (offer) {
56
+    console.log('ColibriSession.sendOffer');
57
+};
58
+
59
+
60
+ColibriSession.prototype.accept = function () {
61
+    console.log('ColibriSession.accept');
62
+};
63
+
64
+ColibriSession.prototype.terminate = function (reason) {
65
+    this.colibri.terminate(this, reason);
66
+};
67
+
68
+ColibriSession.prototype.active = function () {
69
+    console.log('ColibriSession.active');
70
+};
71
+
72
+ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
73
+    this.colibri.setRemoteDescription(this, elem, desctype);
74
+};
75
+
76
+ColibriSession.prototype.addIceCandidate = function (elem) {
77
+    this.colibri.addIceCandidate(this, elem);
78
+};
79
+
80
+ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
81
+    console.log('ColibriSession.sendAnswer');
82
+};
83
+
84
+ColibriSession.prototype.sendTerminate = function (reason, text) {
85
+    console.log('ColibriSession.sendTerminate');
86
+};

+ 381
- 0
libs/strophe/strophe.jingle.adapter.js 查看文件

@@ -0,0 +1,381 @@
1
+function TraceablePeerConnection(ice_config, constraints) {
2
+    var self = this;
3
+    var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
4
+    this.peerconnection = new RTCPeerconnection(ice_config, constraints);
5
+    this.updateLog = [];
6
+    this.stats = {};
7
+    this.statsinterval = null;
8
+    this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
9
+
10
+    // override as desired
11
+    this.trace = function(what, info) {
12
+        //console.warn('WTRACE', what, info);
13
+        self.updateLog.push({
14
+            time: new Date(),
15
+            type: what,
16
+            value: info || ""
17
+        });
18
+    };
19
+    this.onicecandidate = null;
20
+    this.peerconnection.onicecandidate = function (event) {
21
+        self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
22
+        if (self.onicecandidate !== null) {
23
+            self.onicecandidate(event);
24
+        }
25
+    };
26
+    this.onaddstream = null;
27
+    this.peerconnection.onaddstream = function (event) {
28
+        self.trace('onaddstream', event.stream.id);
29
+        if (self.onaddstream !== null) {
30
+            self.onaddstream(event);
31
+        }
32
+    };
33
+    this.onremovestream = null;
34
+    this.peerconnection.onremovestream = function (event) {
35
+        self.trace('onremovestream', event.stream.id);
36
+        if (self.onremovestream !== null) {
37
+            self.onremovestream(event);
38
+        }
39
+    };
40
+    this.onsignalingstatechange = null;
41
+    this.peerconnection.onsignalingstatechange = function (event) {
42
+        self.trace('onsignalingstatechange', event.srcElement.signalingState);
43
+        if (self.onsignalingstatechange !== null) {
44
+            self.onsignalingstatechange(event);
45
+        }
46
+    };
47
+    this.oniceconnectionstatechange = null;
48
+    this.peerconnection.oniceconnectionstatechange = function (event) {
49
+        self.trace('oniceconnectionstatechange', event.srcElement.iceConnectionState);
50
+        if (self.oniceconnectionstatechange !== null) {
51
+            self.oniceconnectionstatechange(event);
52
+        }
53
+    };
54
+    this.onnegotiationneeded = null;
55
+    this.peerconnection.onnegotiationneeded = function (event) {
56
+        self.trace('onnegotiationneeded');
57
+        if (self.onnegotiationneeded !== null) {
58
+            self.onnegotiationneeded(event);
59
+        }
60
+    };
61
+    self.ondatachannel = null;
62
+    this.peerconnection.ondatachannel = function (event) {
63
+        self.trace('ondatachannel', event);
64
+        if (self.ondatachannel !== null) {
65
+            self.ondatachannel(event);
66
+        }
67
+    }
68
+    if (!navigator.mozGetUserMedia) {
69
+        this.statsinterval = window.setInterval(function() {
70
+            self.peerconnection.getStats(function(stats) {
71
+                var results = stats.result();
72
+                for (var i = 0; i < results.length; ++i) {
73
+                    //console.log(results[i].type, results[i].id, results[i].names())
74
+                    var now = new Date();
75
+                    results[i].names().forEach(function (name) {
76
+                        var id = results[i].id + '-' + name;
77
+                        if (!self.stats[id]) {
78
+                            self.stats[id] = {
79
+                                startTime: now,
80
+                                endTime: now,
81
+                                values: [],
82
+                                times: []
83
+                            };
84
+                        }
85
+                        self.stats[id].values.push(results[i].stat(name));
86
+                        self.stats[id].times.push(now.getTime());
87
+                        if (self.stats[id].values.length > self.maxstats) {
88
+                            self.stats[id].values.shift();
89
+                            self.stats[id].times.shift();
90
+                        }
91
+                        self.stats[id].endTime = now;
92
+                    });
93
+                }
94
+            });
95
+
96
+        }, 1000);
97
+    }
98
+};
99
+
100
+dumpSDP = function(description) {
101
+    return 'type: ' + description.type + '\r\n' + description.sdp;
102
+}
103
+
104
+if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
105
+    TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
106
+    TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
107
+    TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() { return this.peerconnection.localDescription; });
108
+    TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() { return this.peerconnection.remoteDescription; });
109
+}
110
+
111
+TraceablePeerConnection.prototype.addStream = function (stream) {
112
+    this.trace('addStream', stream.id);
113
+    this.peerconnection.addStream(stream);
114
+};
115
+
116
+TraceablePeerConnection.prototype.removeStream = function (stream) {
117
+    this.trace('removeStream', stream.id);
118
+    this.peerconnection.removeStream(stream);
119
+};
120
+
121
+TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
122
+    this.trace('createDataChannel', label, opts);
123
+    this.peerconnection.createDataChannel(label, opts);
124
+}
125
+
126
+TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
127
+    var self = this;
128
+    this.trace('setLocalDescription', dumpSDP(description));
129
+    this.peerconnection.setLocalDescription(description,
130
+        function () {
131
+            self.trace('setLocalDescriptionOnSuccess');
132
+            successCallback();
133
+        },
134
+        function (err) {
135
+            self.trace('setLocalDescriptionOnFailure', err);
136
+            failureCallback(err);
137
+        }
138
+    );
139
+    /*
140
+     if (this.statsinterval === null && this.maxstats > 0) {
141
+     // start gathering stats
142
+     }
143
+     */
144
+};
145
+
146
+TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
147
+    var self = this;
148
+    this.trace('setRemoteDescription', dumpSDP(description));
149
+    this.peerconnection.setRemoteDescription(description,
150
+        function () {
151
+            self.trace('setRemoteDescriptionOnSuccess');
152
+            successCallback();
153
+        },
154
+        function (err) {
155
+            self.trace('setRemoteDescriptionOnFailure', err);
156
+            failureCallback(err);
157
+        }
158
+    );
159
+    /*
160
+     if (this.statsinterval === null && this.maxstats > 0) {
161
+     // start gathering stats
162
+     }
163
+     */
164
+};
165
+
166
+TraceablePeerConnection.prototype.close = function () {
167
+    this.trace('stop');
168
+    if (this.statsinterval !== null) {
169
+        window.clearInterval(this.statsinterval);
170
+        this.statsinterval = null;
171
+    }
172
+    this.peerconnection.close();
173
+};
174
+
175
+TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
176
+    var self = this;
177
+    this.trace('createOffer', JSON.stringify(constraints, null, ' '));
178
+    this.peerconnection.createOffer(
179
+        function (offer) {
180
+            self.trace('createOfferOnSuccess', dumpSDP(offer));
181
+            successCallback(offer);
182
+        },
183
+        function(err) {
184
+            self.trace('createOfferOnFailure', err);
185
+            failureCallback(err);
186
+        },
187
+        constraints
188
+    );
189
+};
190
+
191
+TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
192
+    var self = this;
193
+    this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
194
+    this.peerconnection.createAnswer(
195
+        function (answer) {
196
+            self.trace('createAnswerOnSuccess', dumpSDP(answer));
197
+            successCallback(answer);
198
+        },
199
+        function(err) {
200
+            self.trace('createAnswerOnFailure', err);
201
+            failureCallback(err);
202
+        },
203
+        constraints
204
+    );
205
+};
206
+
207
+TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
208
+    var self = this;
209
+    this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
210
+    this.peerconnection.addIceCandidate(candidate);
211
+    /* maybe later
212
+     this.peerconnection.addIceCandidate(candidate,
213
+     function () {
214
+     self.trace('addIceCandidateOnSuccess');
215
+     successCallback();
216
+     },
217
+     function (err) {
218
+     self.trace('addIceCandidateOnFailure', err);
219
+     failureCallback(err);
220
+     }
221
+     );
222
+     */
223
+};
224
+
225
+TraceablePeerConnection.prototype.getStats = function(callback, errback) {
226
+    if (navigator.mozGetUserMedia) {
227
+        // ignore for now...
228
+    } else {
229
+        this.peerconnection.getStats(callback);
230
+    }
231
+};
232
+
233
+// mozilla chrome compat layer -- very similar to adapter.js
234
+function setupRTC() {
235
+    var RTC = null;
236
+    if (navigator.mozGetUserMedia) {
237
+        console.log('This appears to be Firefox');
238
+        var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
239
+        if (version >= 22) {
240
+            RTC = {
241
+                peerconnection: mozRTCPeerConnection,
242
+                browser: 'firefox',
243
+                getUserMedia: navigator.mozGetUserMedia.bind(navigator),
244
+                attachMediaStream: function (element, stream) {
245
+                    element[0].mozSrcObject = stream;
246
+                    element[0].play();
247
+                },
248
+                pc_constraints: {}
249
+            };
250
+            if (!MediaStream.prototype.getVideoTracks)
251
+                MediaStream.prototype.getVideoTracks = function () { return []; };
252
+            if (!MediaStream.prototype.getAudioTracks)
253
+                MediaStream.prototype.getAudioTracks = function () { return []; };
254
+            RTCSessionDescription = mozRTCSessionDescription;
255
+            RTCIceCandidate = mozRTCIceCandidate;
256
+        }
257
+    } else if (navigator.webkitGetUserMedia) {
258
+        console.log('This appears to be Chrome');
259
+        RTC = {
260
+            peerconnection: webkitRTCPeerConnection,
261
+            browser: 'chrome',
262
+            getUserMedia: navigator.webkitGetUserMedia.bind(navigator),
263
+            attachMediaStream: function (element, stream) {
264
+                element.attr('src', webkitURL.createObjectURL(stream));
265
+            },
266
+            // DTLS should now be enabled by default but..
267
+            pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]}
268
+        };
269
+        if (navigator.userAgent.indexOf('Android') != -1) {
270
+            RTC.pc_constraints = {}; // disable DTLS on Android
271
+        }
272
+        if (!webkitMediaStream.prototype.getVideoTracks) {
273
+            webkitMediaStream.prototype.getVideoTracks = function () {
274
+                return this.videoTracks;
275
+            };
276
+        }
277
+        if (!webkitMediaStream.prototype.getAudioTracks) {
278
+            webkitMediaStream.prototype.getAudioTracks = function () {
279
+                return this.audioTracks;
280
+            };
281
+        }
282
+    }
283
+    if (RTC === null) {
284
+        try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { }
285
+    }
286
+    return RTC;
287
+}
288
+
289
+function getUserMediaWithConstraints(um, resolution, bandwidth, fps) {
290
+    var constraints = {audio: false, video: false};
291
+
292
+    if (um.indexOf('video') >= 0) {
293
+        constraints.video = {mandatory: {}};// same behaviour as true
294
+    }
295
+    if (um.indexOf('audio') >= 0) {
296
+        constraints.audio = {};// same behaviour as true
297
+    }
298
+    if (um.indexOf('screen') >= 0) {
299
+        constraints.video = {
300
+            "mandatory": {
301
+                "chromeMediaSource": "screen"
302
+            }
303
+        };
304
+    }
305
+
306
+    if (resolution && !constraints.video) {
307
+        constraints.video = {mandatory: {}};// same behaviour as true
308
+    }
309
+    // see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions
310
+    switch (resolution) {
311
+        // 16:9 first
312
+        case '1080':
313
+        case 'fullhd':
314
+            constraints.video.mandatory.minWidth = 1920;
315
+            constraints.video.mandatory.minHeight = 1080;
316
+            constraints.video.mandatory.minAspectRatio = 1.77;
317
+            break;
318
+        case '720':
319
+        case 'hd':
320
+            constraints.video.mandatory.minWidth = 1280;
321
+            constraints.video.mandatory.minHeight = 720;
322
+            constraints.video.mandatory.minAspectRatio = 1.77;
323
+            break;
324
+        case '360':
325
+            constraints.video.mandatory.minWidth = 640;
326
+            constraints.video.mandatory.minHeight = 360;
327
+            constraints.video.mandatory.minAspectRatio = 1.77;
328
+            break;
329
+        case '180':
330
+            constraints.video.mandatory.minWidth = 320;
331
+            constraints.video.mandatory.minHeight = 180;
332
+            constraints.video.mandatory.minAspectRatio = 1.77;
333
+            break;
334
+        // 4:3
335
+        case '960':
336
+            constraints.video.mandatory.minWidth = 960;
337
+            constraints.video.mandatory.minHeight = 720;
338
+            break;
339
+        case '640':
340
+        case 'vga':
341
+            constraints.video.mandatory.minWidth = 640;
342
+            constraints.video.mandatory.minHeight = 480;
343
+            break;
344
+        case '320':
345
+            constraints.video.mandatory.minWidth = 320;
346
+            constraints.video.mandatory.minHeight = 240;
347
+            break;
348
+        default:
349
+            if (navigator.userAgent.indexOf('Android') != -1) {
350
+                constraints.video.mandatory.minWidth = 320;
351
+                constraints.video.mandatory.minHeight = 240;
352
+                constraints.video.mandatory.maxFrameRate = 15;
353
+            }
354
+            break;
355
+    }
356
+
357
+    if (bandwidth) { // doesn't work currently, see webrtc issue 1846
358
+        if (!constraints.video) constraints.video = {mandatory: {}};//same behaviour as true
359
+        constraints.video.optional = [{bandwidth: bandwidth}];
360
+    }
361
+    if (fps) { // for some cameras it might be necessary to request 30fps
362
+        // so they choose 30fps mjpg over 10fps yuy2
363
+        if (!constraints.video) constraints.video = {mandatory: {}};// same behaviour as tru;
364
+        constraints.video.mandatory.minFrameRate = fps;
365
+    }
366
+
367
+    try {
368
+        RTC.getUserMedia(constraints,
369
+            function (stream) {
370
+                console.log('onUserMediaSuccess');
371
+                $(document).trigger('mediaready.jingle', [stream]);
372
+            },
373
+            function (error) {
374
+                console.warn('Failed to get access to local media. Error ', error);
375
+                $(document).trigger('mediafailure.jingle');
376
+            });
377
+    } catch (e) {
378
+        console.error('GUM failed: ', e);
379
+        $(document).trigger('mediafailure.jingle');
380
+    }
381
+}

+ 4
- 0
libs/strophe/strophe.jingle.bundle.js
文件差异内容过多而无法显示
查看文件


+ 260
- 0
libs/strophe/strophe.jingle.js 查看文件

@@ -0,0 +1,260 @@
1
+/* jshint -W117 */
2
+Strophe.addConnectionPlugin('jingle', {
3
+    connection: null,
4
+    sessions: {},
5
+    jid2session: {},
6
+    ice_config: {iceServers: []},
7
+    pc_constraints: {},
8
+    media_constraints: {
9
+        mandatory: {
10
+            'OfferToReceiveAudio': true,
11
+            'OfferToReceiveVideo': true
12
+        }
13
+        // MozDontOfferDataChannel: true when this is firefox
14
+    },
15
+    localStream: null,
16
+
17
+    init: function (conn) {
18
+        this.connection = conn;
19
+        if (this.connection.disco) {
20
+            // http://xmpp.org/extensions/xep-0167.html#support
21
+            // http://xmpp.org/extensions/xep-0176.html#support
22
+            this.connection.disco.addFeature('urn:xmpp:jingle:1');
23
+            this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
24
+            this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
25
+            this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
26
+            this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
27
+
28
+
29
+            // this is dealt with by SDP O/A so we don't need to annouce this
30
+            //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
31
+            //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
32
+            this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
33
+            //this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
34
+            //this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
35
+        }
36
+        this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
37
+    },
38
+    onJingle: function (iq) {
39
+        var sid = $(iq).find('jingle').attr('sid');
40
+        var action = $(iq).find('jingle').attr('action');
41
+        // send ack first
42
+        var ack = $iq({type: 'result',
43
+            to: iq.getAttribute('from'),
44
+            id: iq.getAttribute('id')
45
+        });
46
+        console.log('on jingle ' + action);
47
+        var sess = this.sessions[sid];
48
+        if ('session-initiate' != action) {
49
+            if (sess === null) {
50
+                ack.type = 'error';
51
+                ack.c('error', {type: 'cancel'})
52
+                    .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
53
+                    .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
54
+                this.connection.send(ack);
55
+                return true;
56
+            }
57
+            // compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
58
+            // local jid is not checked
59
+            if (Strophe.getBareJidFromJid(iq.getAttribute('from')) != Strophe.getBareJidFromJid(sess.peerjid)) {
60
+                console.warn('jid mismatch for session id', sid, iq.getAttribute('from'), sess.peerjid);
61
+                ack.type = 'error';
62
+                ack.c('error', {type: 'cancel'})
63
+                    .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
64
+                    .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
65
+                this.connection.send(ack);
66
+                return true;
67
+            }
68
+        } else if (sess !== undefined) {
69
+            // existing session with same session id
70
+            // this might be out-of-order if the sess.peerjid is the same as from
71
+            ack.type = 'error';
72
+            ack.c('error', {type: 'cancel'})
73
+                .c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
74
+            console.warn('duplicate session id', sid);
75
+            this.connection.send(ack);
76
+            return true;
77
+        }
78
+        // FIXME: check for a defined action
79
+        this.connection.send(ack);
80
+        // see http://xmpp.org/extensions/xep-0166.html#concepts-session
81
+        switch (action) {
82
+            case 'session-initiate':
83
+                sess = new JingleSession($(iq).attr('to'), $(iq).find('jingle').attr('sid'), this.connection);
84
+                // configure session
85
+                if (this.localStream) {
86
+                    sess.localStreams.push(this.localStream);
87
+                }
88
+                sess.media_constraints = this.media_constraints;
89
+                sess.pc_constraints = this.pc_constraints;
90
+                sess.ice_config = this.ice_config;
91
+
92
+                sess.initiate($(iq).attr('from'), false);
93
+                // FIXME: setRemoteDescription should only be done when this call is to be accepted
94
+                sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
95
+
96
+                this.sessions[sess.sid] = sess;
97
+                this.jid2session[sess.peerjid] = sess;
98
+
99
+                // the callback should either
100
+                // .sendAnswer and .accept
101
+                // or .sendTerminate -- not necessarily synchronus
102
+                $(document).trigger('callincoming.jingle', [sess.sid]);
103
+                break;
104
+            case 'session-accept':
105
+                sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
106
+                sess.accept();
107
+                $(document).trigger('callaccepted.jingle', [sess.sid]);
108
+                break;
109
+            case 'session-terminate':
110
+                console.log('terminating...');
111
+                sess.terminate();
112
+                this.terminate(sess.sid);
113
+                if ($(iq).find('>jingle>reason').length) {
114
+                    $(document).trigger('callterminated.jingle', [
115
+                        sess.sid,
116
+                        $(iq).find('>jingle>reason>:first')[0].tagName,
117
+                        $(iq).find('>jingle>reason>text').text()
118
+                    ]);
119
+                } else {
120
+                    $(document).trigger('callterminated.jingle', [sess.sid]);
121
+                }
122
+                break;
123
+            case 'transport-info':
124
+                sess.addIceCandidate($(iq).find('>jingle>content'));
125
+                break;
126
+            case 'session-info':
127
+                var affected;
128
+                if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
129
+                    $(document).trigger('ringing.jingle', [sess.sid]);
130
+                } else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
131
+                    affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
132
+                    $(document).trigger('mute.jingle', [sess.sid, affected]);
133
+                } else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
134
+                    affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
135
+                    $(document).trigger('unmute.jingle', [sess.sid, affected]);
136
+                }
137
+                break;
138
+            case 'addsource': // FIXME: proprietary
139
+                sess.addSource($(iq).find('>jingle>content'));
140
+                break;
141
+            case 'removesource': // FIXME: proprietary
142
+                sess.removeSource($(iq).find('>jingle>content'));
143
+                break;
144
+            default:
145
+                console.warn('jingle action not implemented', action);
146
+                break;
147
+        }
148
+        return true;
149
+    },
150
+    initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
151
+        var sess = new JingleSession(myjid || this.connection.jid,
152
+            Math.random().toString(36).substr(2, 12), // random string
153
+            this.connection);
154
+        // configure session
155
+        if (this.localStream) {
156
+            sess.localStreams.push(this.localStream);
157
+        }
158
+        sess.media_constraints = this.media_constraints;
159
+        sess.pc_constraints = this.pc_constraints;
160
+        sess.ice_config = this.ice_config;
161
+
162
+        sess.initiate(peerjid, true);
163
+        this.sessions[sess.sid] = sess;
164
+        this.jid2session[sess.peerjid] = sess;
165
+        sess.sendOffer();
166
+        return sess;
167
+    },
168
+    terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
169
+        if (sid === null || sid === undefined) {
170
+            for (sid in this.sessions) {
171
+                if (this.sessions[sid].state != 'ended') {
172
+                    this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
173
+                    this.sessions[sid].terminate();
174
+                }
175
+                delete this.jid2session[this.sessions[sid].peerjid];
176
+                delete this.sessions[sid];
177
+            }
178
+        } else if (this.sessions.hasOwnProperty(sid)) {
179
+            if (this.sessions[sid].state != 'ended') {
180
+                this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
181
+                this.sessions[sid].terminate();
182
+            }
183
+            delete this.jid2session[this.sessions[sid].peerjid];
184
+            delete this.sessions[sid];
185
+        }
186
+    },
187
+    terminateByJid: function (jid) {
188
+        if (this.jid2session.hasOwnProperty(jid)) {
189
+            var sess = this.jid2session[jid];
190
+            if (sess) {
191
+                sess.terminate();
192
+                console.log('peer went away silently', jid);
193
+                delete this.sessions[sess.sid];
194
+                delete this.jid2session[jid];
195
+                $(document).trigger('callterminated.jingle', [sess.sid, 'gone']);
196
+            }
197
+        }
198
+    },
199
+    getStunAndTurnCredentials: function () {
200
+        // get stun and turn configuration from server via xep-0215
201
+        // uses time-limited credentials as described in
202
+        // http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
203
+        //
204
+        // see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
205
+        // for a prosody module which implements this
206
+        //
207
+        // currently, this doesn't work with updateIce and therefore credentials with a long
208
+        // validity have to be fetched before creating the peerconnection
209
+        // TODO: implement refresh via updateIce as described in
210
+        //      https://code.google.com/p/webrtc/issues/detail?id=1650
211
+        var self = this;
212
+        this.connection.sendIQ(
213
+            $iq({type: 'get', to: this.connection.domain})
214
+                .c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
215
+            function (res) {
216
+                var iceservers = [];
217
+                $(res).find('>services>service').each(function (idx, el) {
218
+                    el = $(el);
219
+                    var dict = {};
220
+                    switch (el.attr('type')) {
221
+                        case 'stun':
222
+                            dict.url = 'stun:' + el.attr('host');
223
+                            if (el.attr('port')) {
224
+                                dict.url += ':' + el.attr('port');
225
+                            }
226
+                            iceservers.push(dict);
227
+                            break;
228
+                        case 'turn':
229
+                            dict.url = 'turn:';
230
+                            if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
231
+                                if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
232
+                                    dict.url += el.attr('username') + '@';
233
+                                } else {
234
+                                    dict.username = el.attr('username'); // only works in M28
235
+                                }
236
+                            }
237
+                            dict.url += el.attr('host');
238
+                            if (el.attr('port') && el.attr('port') != '3478') {
239
+                                dict.url += ':' + el.attr('port');
240
+                            }
241
+                            if (el.attr('transport') && el.attr('transport') != 'udp') {
242
+                                dict.url += '?transport=' + el.attr('transport');
243
+                            }
244
+                            if (el.attr('password')) {
245
+                                dict.credential = el.attr('password');
246
+                            }
247
+                            iceservers.push(dict);
248
+                            break;
249
+                    }
250
+                });
251
+                self.ice_config.iceServers = iceservers;
252
+            },
253
+            function (err) {
254
+                console.warn('getting turn credentials failed', err);
255
+                console.warn('is mod_turncredentials or similar installed?');
256
+            }
257
+        );
258
+        // implement push?
259
+    }
260
+});

+ 801
- 0
libs/strophe/strophe.jingle.sdp.js 查看文件

@@ -0,0 +1,801 @@
1
+/* jshint -W117 */
2
+// SDP STUFF
3
+function SDP(sdp) {
4
+    this.media = sdp.split('\r\nm=');
5
+    for (var i = 1; i < this.media.length; i++) {
6
+        this.media[i] = 'm=' + this.media[i];
7
+        if (i != this.media.length - 1) {
8
+            this.media[i] += '\r\n';
9
+        }
10
+    }
11
+    this.session = this.media.shift() + '\r\n';
12
+    this.raw = this.session + this.media.join('');
13
+}
14
+
15
+// remove iSAC and CN from SDP
16
+SDP.prototype.mangle = function () {
17
+    var i, j, mline, lines, rtpmap, newdesc;
18
+    for (i = 0; i < this.media.length; i++) {
19
+        lines = this.media[i].split('\r\n');
20
+        lines.pop(); // remove empty last element
21
+        mline = SDPUtil.parse_mline(lines.shift());
22
+        if (mline.media != 'audio')
23
+            continue;
24
+        newdesc = '';
25
+        mline.fmt.length = 0;
26
+        for (j = 0; j < lines.length; j++) {
27
+            if (lines[j].substr(0, 9) == 'a=rtpmap:') {
28
+                rtpmap = SDPUtil.parse_rtpmap(lines[j]);
29
+                if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
30
+                    continue;
31
+                mline.fmt.push(rtpmap.id);
32
+                newdesc += lines[j] + '\r\n';
33
+            } else {
34
+                newdesc += lines[j] + '\r\n';
35
+            }
36
+        }
37
+        this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
38
+        this.media[i] += newdesc;
39
+    }
40
+    this.raw = this.session + this.media.join('');
41
+};
42
+
43
+// remove lines matching prefix from session section
44
+SDP.prototype.removeSessionLines = function(prefix) {
45
+    var self = this;
46
+    var lines = SDPUtil.find_lines(this.session, prefix);
47
+    lines.forEach(function(line) {
48
+        self.session = self.session.replace(line + '\r\n', '');
49
+    });
50
+    this.raw = this.session + this.media.join('');
51
+    return lines;
52
+}
53
+// remove lines matching prefix from a media section specified by mediaindex
54
+// TODO: non-numeric mediaindex could match mid
55
+SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
56
+    var self = this;
57
+    var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
58
+    lines.forEach(function(line) {
59
+        self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
60
+    });
61
+    this.raw = this.session + this.media.join('');
62
+    return lines;
63
+}
64
+
65
+// add content's to a jingle element
66
+SDP.prototype.toJingle = function (elem, thecreator) {
67
+    var i, j, k, mline, ssrc, rtpmap, tmp, line, lines;
68
+    var self = this;
69
+    // new bundle plan
70
+    if (SDPUtil.find_line(this.session, 'a=group:')) {
71
+        lines = SDPUtil.find_lines(this.session, 'a=group:');
72
+        for (i = 0; i < lines.length; i++) {
73
+            tmp = lines[i].split(' ');
74
+            var semantics = tmp.shift().substr(8);
75
+            elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics});
76
+            for (j = 0; j < tmp.length; j++) {
77
+                elem.c('content', {name: tmp[j]}).up();
78
+            }
79
+            elem.up();
80
+        }
81
+    }
82
+    // old bundle plan, to be removed
83
+    var bundle = [];
84
+    if (SDPUtil.find_line(this.session, 'a=group:BUNDLE')) {
85
+        bundle = SDPUtil.find_line(this.session, 'a=group:BUNDLE ').split(' ');
86
+        bundle.shift();
87
+    }
88
+    for (i = 0; i < this.media.length; i++) {
89
+        mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
90
+        if (!(mline.media == 'audio' || mline.media == 'video')) {
91
+            continue;
92
+        }
93
+        if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
94
+            ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
95
+        } else {
96
+            ssrc = false;
97
+        }
98
+
99
+        elem.c('content', {creator: thecreator, name: mline.media});
100
+        if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
101
+            // prefer identifier from a=mid if present
102
+            var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
103
+            elem.attrs({ name: mid });
104
+
105
+            // old BUNDLE plan, to be removed
106
+            if (bundle.indexOf(mid) != -1) {
107
+                elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
108
+                bundle.splice(bundle.indexOf(mid), 1);
109
+            }
110
+        }
111
+        if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
112
+            elem.c('description',
113
+                {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
114
+                    media: mline.media });
115
+            if (ssrc) {
116
+                elem.attrs({ssrc: ssrc});
117
+            }
118
+            for (j = 0; j < mline.fmt.length; j++) {
119
+                rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
120
+                elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
121
+                // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
122
+                if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
123
+                    tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
124
+                    for (k = 0; k < tmp.length; k++) {
125
+                        elem.c('parameter', tmp[k]).up();
126
+                    }
127
+                }
128
+                this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
129
+
130
+                elem.up();
131
+            }
132
+            if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
133
+                elem.c('encryption', {required: 1});
134
+                var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
135
+                crypto.forEach(function(line) {
136
+                    elem.c('crypto', SDPUtil.parse_crypto(line)).up();
137
+                });
138
+                elem.up(); // end of encryption
139
+            }
140
+
141
+            if (ssrc) {
142
+                // new style mapping
143
+                elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
144
+                // FIXME: group by ssrc and support multiple different ssrcs
145
+                var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
146
+                ssrclines.forEach(function(line) {
147
+                    idx = line.indexOf(' ');
148
+                    var linessrc = line.substr(0, idx).substr(7);
149
+                    if (linessrc != ssrc) {
150
+                        elem.up();
151
+                        ssrc = linessrc;
152
+                        elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
153
+                    }
154
+                    var kv = line.substr(idx + 1);
155
+                    elem.c('parameter');
156
+                    if (kv.indexOf(':') == -1) {
157
+                        elem.attrs({ name: kv });
158
+                    } else {
159
+                        elem.attrs({ name: kv.split(':', 2)[0] });
160
+                        elem.attrs({ value: kv.split(':', 2)[1] });
161
+                    }
162
+                    elem.up();
163
+                });
164
+                elem.up();
165
+
166
+                // old proprietary mapping, to be removed at some point
167
+                tmp = SDPUtil.parse_ssrc(this.media[i]);
168
+                tmp.xmlns = 'http://estos.de/ns/ssrc';
169
+                tmp.ssrc = ssrc;
170
+                elem.c('ssrc', tmp).up(); // ssrc is part of description
171
+            }
172
+
173
+            if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
174
+                elem.c('rtcp-mux').up();
175
+            }
176
+
177
+            // XEP-0293 -- map a=rtcp-fb:*
178
+            this.RtcpFbToJingle(i, elem, '*');
179
+
180
+            // XEP-0294
181
+            if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
182
+                lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
183
+                for (j = 0; j < lines.length; j++) {
184
+                    tmp = SDPUtil.parse_extmap(lines[j]);
185
+                    elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
186
+                        uri: tmp.uri,
187
+                        id: tmp.value });
188
+                    if (tmp.hasOwnProperty('direction')) {
189
+                        switch (tmp.direction) {
190
+                            case 'sendonly':
191
+                                elem.attrs({senders: 'responder'});
192
+                                break;
193
+                            case 'recvonly':
194
+                                elem.attrs({senders: 'initiator'});
195
+                                break;
196
+                            case 'sendrecv':
197
+                                elem.attrs({senders: 'both'});
198
+                                break;
199
+                            case 'inactive':
200
+                                elem.attrs({senders: 'none'});
201
+                                break;
202
+                        }
203
+                    }
204
+                    // TODO: handle params
205
+                    elem.up();
206
+                }
207
+            }
208
+            elem.up(); // end of description
209
+        }
210
+
211
+        // map ice-ufrag/pwd, dtls fingerprint, candidates
212
+        this.TransportToJingle(i, elem);
213
+
214
+        if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
215
+            elem.attrs({senders: 'both'});
216
+        } else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
217
+            elem.attrs({senders: 'initiator'});
218
+        } else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
219
+            elem.attrs({senders: 'responder'});
220
+        } else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
221
+            elem.attrs({senders: 'none'});
222
+        }
223
+        if (mline.port == '0') {
224
+            // estos hack to reject an m-line
225
+            elem.attrs({senders: 'rejected'});
226
+        }
227
+        elem.up(); // end of content
228
+    }
229
+    elem.up();
230
+    return elem;
231
+};
232
+
233
+SDP.prototype.TransportToJingle = function (mediaindex, elem) {
234
+    var i = mediaindex;
235
+    var tmp;
236
+    var self = this;
237
+    elem.c('transport');
238
+
239
+    // XEP-0320
240
+    var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
241
+    fingerprints.forEach(function(line) {
242
+        tmp = SDPUtil.parse_fingerprint(line);
243
+        tmp.xmlns = 'urn:xmpp:tmp:jingle:apps:dtls:0';
244
+        // tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0'; -- FIXME: update receivers first
245
+        elem.c('fingerprint').t(tmp.fingerprint);
246
+        delete tmp.fingerprint;
247
+        line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
248
+        if (line) {
249
+            tmp.setup = line.substr(8);
250
+        }
251
+        elem.attrs(tmp);
252
+        elem.up(); // end of fingerprint
253
+    });
254
+    tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
255
+    if (tmp) {
256
+        tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
257
+        elem.attrs(tmp);
258
+        // XEP-0176
259
+        if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
260
+            var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
261
+            lines.forEach(function (line) {
262
+                elem.c('candidate', SDPUtil.candidateToJingle(line)).up();
263
+            });
264
+        }
265
+    }
266
+    elem.up(); // end of transport
267
+}
268
+
269
+SDP.prototype.RtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
270
+    var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
271
+    lines.forEach(function (line) {
272
+        var tmp = SDPUtil.parse_rtcpfb(line);
273
+        if (tmp.type == 'trr-int') {
274
+            elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
275
+            elem.up();
276
+        } else {
277
+            elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
278
+            if (tmp.params.length > 0) {
279
+                elem.attrs({'subtype': tmp.params[0]});
280
+            }
281
+            elem.up();
282
+        }
283
+    });
284
+};
285
+
286
+SDP.prototype.RtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
287
+    var media = '';
288
+    var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
289
+    if (tmp.length) {
290
+        media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
291
+        if (tmp.attr('value')) {
292
+            media += tmp.attr('value');
293
+        } else {
294
+            media += '0';
295
+        }
296
+        media += '\r\n';
297
+    }
298
+    tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
299
+    tmp.each(function () {
300
+        media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
301
+        if ($(this).attr('subtype')) {
302
+            media += ' ' + $(this).attr('subtype');
303
+        }
304
+        media += '\r\n';
305
+    });
306
+    return media;
307
+};
308
+
309
+// construct an SDP from a jingle stanza
310
+SDP.prototype.fromJingle = function (jingle) {
311
+    var self = this;
312
+    this.raw = 'v=0\r\n' +
313
+        'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
314
+        's=-\r\n' +
315
+        't=0 0\r\n';
316
+    // http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
317
+    if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
318
+        $(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
319
+            var contents = $(group).find('>content').map(function (idx, content) {
320
+                return content.getAttribute('name');
321
+            }).get();
322
+            if (contents.length > 0) {
323
+                self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
324
+            }
325
+        });
326
+    } else if ($(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').length) {
327
+        // temporary namespace, not to be used. to be removed soon.
328
+        $(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').each(function (idx, group) {
329
+            var contents = $(group).find('>content').map(function (idx, content) {
330
+                return content.getAttribute('name');
331
+            }).get();
332
+            if (group.getAttribute('type') !== null && contents.length > 0) {
333
+                self.raw += 'a=group:' + group.getAttribute('type') + ' ' + contents.join(' ') + '\r\n';
334
+            }
335
+        });
336
+    } else {
337
+        // for backward compability, to be removed soon
338
+        // assume all contents are in the same bundle group, can be improved upon later
339
+        var bundle = $(jingle).find('>content').filter(function (idx, content) {
340
+            //elem.c('bundle', {xmlns:'http://estos.de/ns/bundle'});
341
+            return $(content).find('>bundle').length > 0;
342
+        }).map(function (idx, content) {
343
+                return content.getAttribute('name');
344
+            }).get();
345
+        if (bundle.length) {
346
+            this.raw += 'a=group:BUNDLE ' + bundle.join(' ') + '\r\n';
347
+        }
348
+    }
349
+
350
+    this.session = this.raw;
351
+    jingle.find('>content').each(function () {
352
+        var m = self.jingle2media($(this));
353
+        self.media.push(m);
354
+    });
355
+
356
+    // reconstruct msid-semantic -- apparently not necessary
357
+    /*
358
+     var msid = SDPUtil.parse_ssrc(this.raw);
359
+     if (msid.hasOwnProperty('mslabel')) {
360
+     this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
361
+     }
362
+     */
363
+
364
+    this.raw = this.session + this.media.join('');
365
+};
366
+
367
+// translate a jingle content element into an an SDP media part
368
+SDP.prototype.jingle2media = function (content) {
369
+    var media = '',
370
+        desc = content.find('description'),
371
+        ssrc = desc.attr('ssrc'),
372
+        self = this,
373
+        tmp;
374
+
375
+    tmp = { media: desc.attr('media') };
376
+    tmp.port = '1';
377
+    if (content.attr('senders') == 'rejected') {
378
+        // estos hack to reject an m-line.
379
+        tmp.port = '0';
380
+    }
381
+    if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
382
+        tmp.proto = 'RTP/SAVPF';
383
+    } else {
384
+        tmp.proto = 'RTP/AVPF';
385
+    }
386
+    tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
387
+    media += SDPUtil.build_mline(tmp) + '\r\n';
388
+    media += 'c=IN IP4 0.0.0.0\r\n';
389
+    media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
390
+    tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
391
+    if (tmp.length) {
392
+        if (tmp.attr('ufrag')) {
393
+            media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
394
+        }
395
+        if (tmp.attr('pwd')) {
396
+            media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
397
+        }
398
+        tmp.find('>fingerprint').each(function () {
399
+            // FIXME: check namespace at some point
400
+            media += 'a=fingerprint:' + this.getAttribute('hash');
401
+            media += ' ' + $(this).text();
402
+            media += '\r\n';
403
+            if (this.getAttribute('setup')) {
404
+                media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
405
+            }
406
+        });
407
+    }
408
+    switch (content.attr('senders')) {
409
+        case 'initiator':
410
+            media += 'a=sendonly\r\n';
411
+            break;
412
+        case 'responder':
413
+            media += 'a=recvonly\r\n';
414
+            break;
415
+        case 'none':
416
+            media += 'a=inactive\r\n';
417
+            break;
418
+        case 'both':
419
+            media += 'a=sendrecv\r\n';
420
+            break;
421
+    }
422
+    media += 'a=mid:' + content.attr('name') + '\r\n';
423
+
424
+    // <description><rtcp-mux/></description>
425
+    // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
426
+    // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
427
+    if (desc.find('rtcp-mux').length) {
428
+        media += 'a=rtcp-mux\r\n';
429
+    }
430
+
431
+    if (desc.find('encryption').length) {
432
+        desc.find('encryption>crypto').each(function () {
433
+            media += 'a=crypto:' + this.getAttribute('tag');
434
+            media += ' ' + this.getAttribute('crypto-suite');
435
+            media += ' ' + this.getAttribute('key-params');
436
+            if (this.getAttribute('session-params')) {
437
+                media += ' ' + this.getAttribute('session-params');
438
+            }
439
+            media += '\r\n';
440
+        });
441
+    }
442
+    desc.find('payload-type').each(function () {
443
+        media += SDPUtil.build_rtpmap(this) + '\r\n';
444
+        if ($(this).find('>parameter').length) {
445
+            media += 'a=fmtp:' + this.getAttribute('id') + ' ';
446
+            media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join(';');
447
+            media += '\r\n';
448
+        }
449
+        // xep-0293
450
+        media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
451
+    });
452
+
453
+    // xep-0293
454
+    media += self.RtcpFbFromJingle(desc, '*');
455
+
456
+    // xep-0294
457
+    tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
458
+    tmp.each(function () {
459
+        media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
460
+    });
461
+
462
+    content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
463
+        media += SDPUtil.candidateFromJingle(this);
464
+    });
465
+
466
+    tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
467
+    tmp.each(function () {
468
+        var ssrc = this.getAttribute('ssrc');
469
+        $(this).find('>parameter').each(function () {
470
+            media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
471
+            if (this.getAttribute('value') && this.getAttribute('value').length)
472
+                media += ':' + this.getAttribute('value');
473
+            media += '\r\n';
474
+        });
475
+    });
476
+
477
+    if (tmp.length === 0) {
478
+        // fallback to proprietary mapping of a=ssrc lines
479
+        tmp = content.find('description>ssrc[xmlns="http://estos.de/ns/ssrc"]');
480
+        if (tmp.length) {
481
+            media += 'a=ssrc:' + ssrc + ' cname:' + tmp.attr('cname') + '\r\n';
482
+            media += 'a=ssrc:' + ssrc + ' msid:' + tmp.attr('msid') + '\r\n';
483
+            media += 'a=ssrc:' + ssrc + ' mslabel:' + tmp.attr('mslabel') + '\r\n';
484
+            media += 'a=ssrc:' + ssrc + ' label:' + tmp.attr('label') + '\r\n';
485
+        }
486
+    }
487
+    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
+};

+ 858
- 0
libs/strophe/strophe.jingle.session.js 查看文件

@@ -0,0 +1,858 @@
1
+/* jshint -W117 */
2
+// Jingle stuff
3
+function JingleSession(me, sid, connection) {
4
+    this.me = me;
5
+    this.sid = sid;
6
+    this.connection = connection;
7
+    this.initiator = null;
8
+    this.responder = null;
9
+    this.isInitiator = null;
10
+    this.peerjid = null;
11
+    this.state = null;
12
+    this.peerconnection = null;
13
+    this.remoteStream = null;
14
+    this.localSDP = null;
15
+    this.remoteSDP = null;
16
+    this.localStreams = [];
17
+    this.relayedStreams = [];
18
+    this.remoteStreams = [];
19
+    this.startTime = null;
20
+    this.stopTime = null;
21
+    this.media_constraints = null;
22
+    this.pc_constraints = null;
23
+    this.ice_config = {};
24
+    this.drip_container = [];
25
+
26
+    this.usetrickle = true;
27
+    this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
28
+    this.usedrip = false; // dripping is sending trickle candidates not one-by-one
29
+
30
+    this.hadstuncandidate = false;
31
+    this.hadturncandidate = false;
32
+    this.lasticecandidate = false;
33
+
34
+    this.statsinterval = null;
35
+
36
+    this.reason = null;
37
+
38
+    this.addssrc = [];
39
+    this.removessrc = [];
40
+    this.pendingop = null;
41
+
42
+    this.wait = true;
43
+}
44
+
45
+JingleSession.prototype.initiate = function (peerjid, isInitiator) {
46
+    var self = this;
47
+    if (this.state !== null) {
48
+        console.error('attempt to initiate on session ' + this.sid +
49
+            'in state ' + this.state);
50
+        return;
51
+    }
52
+    this.isInitiator = isInitiator;
53
+    this.state = 'pending';
54
+    this.initiator = isInitiator ? this.me : peerjid;
55
+    this.responder = !isInitiator ? this.me : peerjid;
56
+    this.peerjid = peerjid;
57
+    //console.log('create PeerConnection ' + JSON.stringify(this.ice_config));
58
+    try {
59
+        this.peerconnection = new RTCPeerconnection(this.ice_config,
60
+            this.pc_constraints);
61
+    } catch (e) {
62
+        console.error('Failed to create PeerConnection, exception: ',
63
+            e.message);
64
+        console.error(e);
65
+        return;
66
+    }
67
+    this.hadstuncandidate = false;
68
+    this.hadturncandidate = false;
69
+    this.lasticecandidate = false;
70
+    this.peerconnection.onicecandidate = function (event) {
71
+        self.sendIceCandidate(event.candidate);
72
+    };
73
+    this.peerconnection.onaddstream = function (event) {
74
+        self.remoteStream = event.stream;
75
+        self.remoteStreams.push(event.stream);
76
+        $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
77
+    };
78
+    this.peerconnection.onremovestream = function (event) {
79
+        self.remoteStream = null;
80
+        // FIXME: remove from this.remoteStreams
81
+        $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
82
+    };
83
+    this.peerconnection.onsignalingstatechange = function (event) {
84
+        if (!(self && self.peerconnection)) return;
85
+    };
86
+    this.peerconnection.oniceconnectionstatechange = function (event) {
87
+        if (!(self && self.peerconnection)) return;
88
+        switch (self.peerconnection.iceConnectionState) {
89
+            case 'connected':
90
+                this.startTime = new Date();
91
+                break;
92
+            case 'disconnected':
93
+                this.stopTime = new Date();
94
+                break;
95
+        }
96
+        $(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
97
+    };
98
+    // add any local and relayed stream
99
+    this.localStreams.forEach(function(stream) {
100
+        self.peerconnection.addStream(stream);
101
+    });
102
+    this.relayedStreams.forEach(function(stream) {
103
+        self.peerconnection.addStream(stream);
104
+    });
105
+};
106
+
107
+JingleSession.prototype.accept = function () {
108
+    var self = this;
109
+    this.state = 'active';
110
+
111
+    var pranswer = this.peerconnection.localDescription;
112
+    if (!pranswer || pranswer.type != 'pranswer') {
113
+        return;
114
+    }
115
+    console.log('going from pranswer to answer');
116
+    if (this.usetrickle) {
117
+        // remove candidates already sent from session-accept
118
+        var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
119
+        for (var i = 0; i < lines.length; i++) {
120
+            pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
121
+        }
122
+    }
123
+    while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
124
+        // FIXME: change any inactive to sendrecv or whatever they were originally
125
+        pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
126
+    }
127
+    var prsdp = new SDP(pranswer.sdp);
128
+    var accept = $iq({to: this.peerjid,
129
+        type: 'set'})
130
+        .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
131
+            action: 'session-accept',
132
+            initiator: this.initiator,
133
+            responder: this.responder,
134
+            sid: this.sid });
135
+    prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
136
+    this.connection.sendIQ(accept,
137
+        function () {
138
+            var ack = {};
139
+            ack.source = 'answer';
140
+            $(document).trigger('ack.jingle', [self.sid, ack]);
141
+        },
142
+        function (stanza) {
143
+            var error = ($(stanza).find('error').length) ? {
144
+                code: $(stanza).find('error').attr('code'),
145
+                reason: $(stanza).find('error :first')[0].tagName,
146
+            }:{};
147
+            error.source = 'answer';
148
+            $(document).trigger('error.jingle', [self.sid, error]);
149
+        },
150
+        10000);
151
+
152
+    var sdp = this.peerconnection.localDescription.sdp;
153
+    while (SDPUtil.find_line(sdp, 'a=inactive')) {
154
+        // FIXME: change any inactive to sendrecv or whatever they were originally
155
+        sdp = sdp.replace('a=inactive', 'a=sendrecv');
156
+    }
157
+    this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
158
+        function () {
159
+            //console.log('setLocalDescription success');
160
+            $(document).trigger('setLocalDescription.jingle', [self.sid]);
161
+        },
162
+        function (e) {
163
+            console.error('setLocalDescription failed', e);
164
+        }
165
+    );
166
+};
167
+
168
+JingleSession.prototype.terminate = function (reason) {
169
+    this.state = 'ended';
170
+    this.reason = reason;
171
+    this.peerconnection.close();
172
+    if (this.statsinterval !== null) {
173
+        window.clearInterval(this.statsinterval);
174
+        this.statsinterval = null;
175
+    }
176
+};
177
+
178
+JingleSession.prototype.active = function () {
179
+    return this.state == 'active';
180
+};
181
+
182
+JingleSession.prototype.sendIceCandidate = function (candidate) {
183
+    var self = this;
184
+    if (candidate && !this.lasticecandidate) {
185
+        var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
186
+        var jcand = SDPUtil.candidateToJingle(candidate.candidate);
187
+        if (!(ice && jcand)) {
188
+            console.error('failed to get ice && jcand');
189
+            return;
190
+        }
191
+        ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
192
+
193
+        if (jcand.type === 'srflx') {
194
+            this.hadstuncandidate = true;
195
+        } else if (jcand.type === 'relay') {
196
+            this.hadturncandidate = true;
197
+        }
198
+
199
+        if (this.usetrickle) {
200
+            if (this.usedrip) {
201
+                if (this.drip_container.length === 0) {
202
+                    // start 20ms callout
203
+                    window.setTimeout(function () {
204
+                        if (self.drip_container.length === 0) return;
205
+                        self.sendIceCandidates(self.drip_container);
206
+                        self.drip_container = [];
207
+                    }, 20);
208
+
209
+                }
210
+                this.drip_container.push(event.candidate);
211
+                return;
212
+            } else {
213
+                self.sendIceCandidate([event.candidate]);
214
+            }
215
+        }
216
+    } else {
217
+        //console.log('sendIceCandidate: last candidate.');
218
+        if (!this.usetrickle) {
219
+            //console.log('should send full offer now...');
220
+            var init = $iq({to: this.peerjid,
221
+                type: 'set'})
222
+                .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
223
+                    action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
224
+                    initiator: this.initiator,
225
+                    sid: this.sid});
226
+            this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
227
+            this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
228
+            this.connection.sendIQ(init,
229
+                function () {
230
+                    //console.log('session initiate ack');
231
+                    var ack = {};
232
+                    ack.source = 'offer';
233
+                    $(document).trigger('ack.jingle', [self.sid, ack]);
234
+                },
235
+                function (stanza) {
236
+                    self.state = 'error';
237
+                    self.peerconnection.close();
238
+                    var error = ($(stanza).find('error').length) ? {
239
+                        code: $(stanza).find('error').attr('code'),
240
+                        reason: $(stanza).find('error :first')[0].tagName,
241
+                    }:{};
242
+                    error.source = 'offer';
243
+                    $(document).trigger('error.jingle', [self.sid, error]);
244
+                },
245
+                10000);
246
+        }
247
+        this.lasticecandidate = true;
248
+        console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
249
+        console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
250
+
251
+        if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
252
+            $(document).trigger('nostuncandidates.jingle', [this.sid]);
253
+        }
254
+    }
255
+};
256
+
257
+JingleSession.prototype.sendIceCandidates = function (candidates) {
258
+    console.log('sendIceCandidates', candidates);
259
+    var cand = $iq({to: this.peerjid, type: 'set'})
260
+        .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
261
+            action: 'transport-info',
262
+            initiator: this.initiator,
263
+            sid: this.sid});
264
+    for (var mid = 0; mid < this.localSDP.media.length; mid++) {
265
+        var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
266
+        if (cands.length > 0) {
267
+            var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
268
+            ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
269
+            cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
270
+                name: cands[0].sdpMid
271
+            }).c('transport', ice);
272
+            for (var i = 0; i < cands.length; i++) {
273
+                cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
274
+            }
275
+            // add fingerprint
276
+            if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
277
+                var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
278
+                tmp.required = true;
279
+                cand.c('fingerprint').t(tmp.fingerprint);
280
+                delete tmp.fingerprint;
281
+                cand.attrs(tmp);
282
+                cand.up();
283
+            }
284
+            cand.up(); // transport
285
+            cand.up(); // content
286
+        }
287
+    }
288
+    // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
289
+    //console.log('was this the last candidate', this.lasticecandidate);
290
+    this.connection.sendIQ(cand,
291
+        function () {
292
+            var ack = {};
293
+            ack.source = 'transportinfo';
294
+            $(document).trigger('ack.jingle', [this.sid, ack]);
295
+        },
296
+        function (stanza) {
297
+            var error = ($(stanza).find('error').length) ? {
298
+                code: $(stanza).find('error').attr('code'),
299
+                reason: $(stanza).find('error :first')[0].tagName,
300
+            }:{};
301
+            error.source = 'transportinfo';
302
+            $(document).trigger('error.jingle', [this.sid, error]);
303
+        },
304
+        10000);
305
+};
306
+
307
+
308
+JingleSession.prototype.sendOffer = function () {
309
+    //console.log('sendOffer...');
310
+    var self = this;
311
+    this.peerconnection.createOffer(function (sdp) {
312
+            self.createdOffer(sdp);
313
+        },
314
+        function (e) {
315
+            console.error('createOffer failed', e);
316
+        },
317
+        this.media_constraints
318
+    );
319
+};
320
+
321
+JingleSession.prototype.createdOffer = function (sdp) {
322
+    //console.log('createdOffer', sdp);
323
+    var self = this;
324
+    this.localSDP = new SDP(sdp.sdp);
325
+    //this.localSDP.mangle();
326
+    if (this.usetrickle) {
327
+        var init = $iq({to: this.peerjid,
328
+            type: 'set'})
329
+            .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
330
+                action: 'session-initiate',
331
+                initiator: this.initiator,
332
+                sid: this.sid});
333
+        this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
334
+        this.connection.sendIQ(init,
335
+            function () {
336
+                var ack = {};
337
+                ack.source = 'offer';
338
+                $(document).trigger('ack.jingle', [self.sid, ack]);
339
+            },
340
+            function (stanza) {
341
+                self.state = 'error';
342
+                self.peerconnection.close();
343
+                var error = ($(stanza).find('error').length) ? {
344
+                    code: $(stanza).find('error').attr('code'),
345
+                    reason: $(stanza).find('error :first')[0].tagName,
346
+                }:{};
347
+                error.source = 'offer';
348
+                $(document).trigger('error.jingle', [self.sid, error]);
349
+            },
350
+            10000);
351
+    }
352
+    sdp.sdp = this.localSDP.raw;
353
+    this.peerconnection.setLocalDescription(sdp,
354
+        function () {
355
+            $(document).trigger('setLocalDescription.jingle', [self.sid]);
356
+            //console.log('setLocalDescription success');
357
+        },
358
+        function (e) {
359
+            console.error('setLocalDescription failed', e);
360
+        }
361
+    );
362
+    var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
363
+    for (var i = 0; i < cands.length; i++) {
364
+        var cand = SDPUtil.parse_icecandidate(cands[i]);
365
+        if (cand.type == 'srflx') {
366
+            this.hadstuncandidate = true;
367
+        } else if (cand.type == 'relay') {
368
+            this.hadturncandidate = true;
369
+        }
370
+    }
371
+};
372
+
373
+JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
374
+    //console.log('setting remote description... ', desctype);
375
+    this.remoteSDP = new SDP('');
376
+    this.remoteSDP.fromJingle(elem);
377
+    if (this.peerconnection.remoteDescription !== null) {
378
+        console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
379
+        if (this.peerconnection.remoteDescription.type == 'pranswer') {
380
+            var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
381
+            for (var i = 0; i < pranswer.media.length; i++) {
382
+                // make sure we have ice ufrag and pwd
383
+                if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
384
+                    if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
385
+                        this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
386
+                    } else {
387
+                        console.warn('no ice ufrag?');
388
+                    }
389
+                    if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
390
+                        this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
391
+                    } else {
392
+                        console.warn('no ice pwd?');
393
+                    }
394
+                }
395
+                // copy over candidates
396
+                var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
397
+                for (var j = 0; j < lines.length; j++) {
398
+                    this.remoteSDP.media[i] += lines[j] + '\r\n';
399
+                }
400
+            }
401
+            this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
402
+        }
403
+    }
404
+    var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
405
+
406
+    this.peerconnection.setRemoteDescription(remotedesc,
407
+        function () {
408
+            //console.log('setRemoteDescription success');
409
+        },
410
+        function (e) {
411
+            console.error('setRemoteDescription error', e);
412
+        }
413
+    );
414
+};
415
+
416
+JingleSession.prototype.addIceCandidate = function (elem) {
417
+    var self = this;
418
+    if (this.peerconnection.signalingState == 'closed') {
419
+        return;
420
+    }
421
+    if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
422
+        console.log('trickle ice candidate arriving before session accept...');
423
+        // create a PRANSWER for setRemoteDescription
424
+        if (!this.remoteSDP) {
425
+            var cobbled = 'v=0\r\n' +
426
+                'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
427
+                's=-\r\n' +
428
+                't=0 0\r\n';
429
+            // first, take some things from the local description
430
+            for (var i = 0; i < this.localSDP.media.length; i++) {
431
+                cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
432
+                cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
433
+                if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
434
+                    cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
435
+                }
436
+                cobbled += 'a=inactive\r\n';
437
+            }
438
+            this.remoteSDP = new SDP(cobbled);
439
+        }
440
+        // then add things like ice and dtls from remote candidate
441
+        elem.each(function () {
442
+            for (var i = 0; i < self.remoteSDP.media.length; i++) {
443
+                if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
444
+                    self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
445
+                    if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
446
+                        var tmp = $(this).find('transport');
447
+                        self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
448
+                        self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
449
+                        tmp = $(this).find('transport>fingerprint');
450
+                        if (tmp.length) {
451
+                            self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
452
+                        } else {
453
+                            console.log('no dtls fingerprint (webrtc issue #1718?)');
454
+                            self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
455
+                        }
456
+                        break;
457
+                    }
458
+                }
459
+            }
460
+        });
461
+        this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
462
+
463
+        // we need a complete SDP with ice-ufrag/ice-pwd in all parts
464
+        // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
465
+        // but it could be in the session part as well. since the code above constructs this sdp this can't happen however
466
+        var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
467
+            return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
468
+        }).length == this.remoteSDP.media.length;
469
+
470
+        if (iscomplete) {
471
+            console.log('setting pranswer');
472
+            try {
473
+                this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
474
+                    function() {
475
+                    },
476
+                    function(e) {
477
+                        console.log('setRemoteDescription pranswer failed', e.toString());
478
+                    });
479
+            } catch (e) {
480
+                console.error('setting pranswer failed', e);
481
+            }
482
+        } else {
483
+            //console.log('not yet setting pranswer');
484
+        }
485
+    }
486
+    // operate on each content element
487
+    elem.each(function () {
488
+        // would love to deactivate this, but firefox still requires it
489
+        var idx = -1;
490
+        var i;
491
+        for (i = 0; i < self.remoteSDP.media.length; i++) {
492
+            if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
493
+                self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
494
+                idx = i;
495
+                break;
496
+            }
497
+        }
498
+        if (idx == -1) { // fall back to localdescription
499
+            for (i = 0; i < self.localSDP.media.length; i++) {
500
+                if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
501
+                    self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
502
+                    idx = i;
503
+                    break;
504
+                }
505
+            }
506
+        }
507
+        var name = $(this).attr('name');
508
+        // TODO: check ice-pwd and ice-ufrag?
509
+        $(this).find('transport>candidate').each(function () {
510
+            var line, candidate;
511
+            line = SDPUtil.candidateFromJingle(this);
512
+            candidate = new RTCIceCandidate({sdpMLineIndex: idx,
513
+                sdpMid: name,
514
+                candidate: line});
515
+            try {
516
+                self.peerconnection.addIceCandidate(candidate);
517
+            } catch (e) {
518
+                console.error('addIceCandidate failed', e.toString(), line);
519
+            }
520
+        });
521
+    });
522
+};
523
+
524
+JingleSession.prototype.sendAnswer = function (provisional) {
525
+    //console.log('createAnswer', provisional);
526
+    var self = this;
527
+    this.peerconnection.createAnswer(
528
+        function (sdp) {
529
+            self.createdAnswer(sdp, provisional);
530
+        },
531
+        function (e) {
532
+            console.error('createAnswer failed', e);
533
+        },
534
+        this.media_constraints
535
+    );
536
+};
537
+
538
+JingleSession.prototype.createdAnswer = function (sdp, provisional) {
539
+    //console.log('createAnswer callback');
540
+    var self = this;
541
+    this.localSDP = new SDP(sdp.sdp);
542
+    //this.localSDP.mangle();
543
+    this.usepranswer = provisional === true;
544
+    if (this.usetrickle) {
545
+        if (!this.usepranswer) {
546
+            var accept = $iq({to: this.peerjid,
547
+                type: 'set'})
548
+                .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
549
+                    action: 'session-accept',
550
+                    initiator: this.initiator,
551
+                    responder: this.responder,
552
+                    sid: this.sid });
553
+            this.localSDP.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
554
+            this.connection.sendIQ(accept,
555
+                function () {
556
+                    var ack = {};
557
+                    ack.source = 'answer';
558
+                    $(document).trigger('ack.jingle', [self.sid, ack]);
559
+                },
560
+                function (stanza) {
561
+                    var error = ($(stanza).find('error').length) ? {
562
+                        code: $(stanza).find('error').attr('code'),
563
+                        reason: $(stanza).find('error :first')[0].tagName,
564
+                    }:{};
565
+                    error.source = 'answer';
566
+                    $(document).trigger('error.jingle', [self.sid, error]);
567
+                },
568
+                10000);
569
+        } else {
570
+            sdp.type = 'pranswer';
571
+            for (var i = 0; i < this.localSDP.media.length; i++) {
572
+                this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
573
+            }
574
+            this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
575
+        }
576
+    }
577
+    sdp.sdp = this.localSDP.raw;
578
+    this.peerconnection.setLocalDescription(sdp,
579
+        function () {
580
+            $(document).trigger('setLocalDescription.jingle', [self.sid]);
581
+            //console.log('setLocalDescription success');
582
+        },
583
+        function (e) {
584
+            console.error('setLocalDescription failed', e);
585
+        }
586
+    );
587
+    var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
588
+    for (var j = 0; j < cands.length; j++) {
589
+        var cand = SDPUtil.parse_icecandidate(cands[j]);
590
+        if (cand.type == 'srflx') {
591
+            this.hadstuncandidate = true;
592
+        } else if (cand.type == 'relay') {
593
+            this.hadturncandidate = true;
594
+        }
595
+    }
596
+};
597
+
598
+JingleSession.prototype.sendTerminate = function (reason, text) {
599
+    var self = this,
600
+        term = $iq({to: this.peerjid,
601
+            type: 'set'})
602
+            .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
603
+                action: 'session-terminate',
604
+                initiator: this.initiator,
605
+                sid: this.sid})
606
+            .c('reason')
607
+            .c(reason || 'success');
608
+
609
+    if (text) {
610
+        term.up().c('text').t(text);
611
+    }
612
+
613
+    this.connection.sendIQ(term,
614
+        function () {
615
+            self.peerconnection.close();
616
+            self.peerconnection = null;
617
+            self.terminate();
618
+            var ack = {};
619
+            ack.source = 'terminate';
620
+            $(document).trigger('ack.jingle', [self.sid, ack]);
621
+        },
622
+        function (stanza) {
623
+            var error = ($(stanza).find('error').length) ? {
624
+                code: $(stanza).find('error').attr('code'),
625
+                reason: $(stanza).find('error :first')[0].tagName,
626
+            }:{};
627
+            $(document).trigger('ack.jingle', [self.sid, error]);
628
+        },
629
+        10000);
630
+    if (this.statsinterval !== null) {
631
+        window.clearInterval(this.statsinterval);
632
+        this.statsinterval = null;
633
+    }
634
+};
635
+
636
+
637
+JingleSession.prototype.addSource = function (elem) {
638
+    console.log('addssrc', new Date().getTime());
639
+    console.log('ice', this.peerconnection.iceConnectionState);
640
+    var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
641
+
642
+    var self = this;
643
+    $(elem).each(function (idx, content) {
644
+        var name = $(content).attr('name');
645
+        var lines = '';
646
+        tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
647
+        tmp.each(function () {
648
+            var ssrc = $(this).attr('ssrc');
649
+            $(this).find('>parameter').each(function () {
650
+                lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
651
+                if ($(this).attr('value') && $(this).attr('value').length)
652
+                    lines += ':' + $(this).attr('value');
653
+                lines += '\r\n';
654
+            });
655
+        });
656
+        sdp.media.forEach(function(media, idx) {
657
+            if (!SDPUtil.find_line(media, 'a=mid:' + name))
658
+                return;
659
+            sdp.media[idx] += lines;
660
+            if (!self.addssrc[idx]) self.addssrc[idx] = '';
661
+            self.addssrc[idx] += lines;
662
+        });
663
+        sdp.raw = sdp.session + sdp.media.join('');
664
+    });
665
+    this.modifySources();
666
+};
667
+
668
+JingleSession.prototype.removeSource = function (elem) {
669
+    console.log('removessrc', new Date().getTime());
670
+    console.log('ice', this.peerconnection.iceConnectionState);
671
+    var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
672
+
673
+    var self = this;
674
+    $(elem).each(function (idx, content) {
675
+        var name = $(content).attr('name');
676
+        var lines = '';
677
+        tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
678
+        tmp.each(function () {
679
+            var ssrc = $(this).attr('ssrc');
680
+            $(this).find('>parameter').each(function () {
681
+                lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
682
+                if ($(this).attr('value') && $(this).attr('value').length)
683
+                    lines += ':' + $(this).attr('value');
684
+                lines += '\r\n';
685
+            });
686
+        });
687
+        sdp.media.forEach(function(media, idx) {
688
+            if (!SDPUtil.find_line(media, 'a=mid:' + name))
689
+                return;
690
+            sdp.media[idx] += lines;
691
+            if (!self.removessrc[idx]) self.removessrc[idx] = '';
692
+            self.removessrc[idx] += lines;
693
+        });
694
+        sdp.raw = sdp.session + sdp.media.join('');
695
+    });
696
+    this.modifySources();
697
+};
698
+
699
+JingleSession.prototype.modifySources = function() {
700
+    var self = this;
701
+    if (this.peerconnection.signalingState == 'closed') return;
702
+    if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)) return;
703
+    if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
704
+        console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
705
+        this.wait = true;
706
+        window.setTimeout(function() { self.modifySources(); }, 250);
707
+        return;
708
+    }
709
+    if (this.wait) {
710
+        window.setTimeout(function() { self.modifySources(); }, 2500);
711
+        this.wait = false;
712
+        return;
713
+    }
714
+
715
+    var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
716
+
717
+    // add sources
718
+    this.addssrc.forEach(function(lines, idx) {
719
+        sdp.media[idx] += lines;
720
+    });
721
+    this.addssrc = [];
722
+
723
+    // remove sources
724
+    this.removessrc.forEach(function(lines, idx) {
725
+        lines = lines.split('\r\n');
726
+        lines.pop(); // remove empty last element;
727
+        lines.forEach(function(line) {
728
+            sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
729
+        });
730
+    });
731
+    this.removessrc = [];
732
+
733
+    sdp.raw = sdp.session + sdp.media.join('');
734
+    this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
735
+        function() {
736
+            self.peerconnection.createAnswer(
737
+                function(modifiedAnswer) {
738
+                    // change video direction, see https://github.com/jitsi/jitmeet/issues/41
739
+                    if (self.pendingop !== null) {
740
+                        var sdp = new SDP(modifiedAnswer.sdp);
741
+                        if (sdp.media.length > 1) {
742
+                            switch(self.pendingop) {
743
+                                case 'mute':
744
+                                    sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
745
+                                    break;
746
+                                case 'unmute':
747
+                                    sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
748
+                                    break;
749
+                            }
750
+                            sdp.raw = sdp.session + sdp.media.join('');
751
+                            modifiedAnswer.sdp = sdp.raw;
752
+                        }
753
+                        self.pendingop = null;
754
+                    }
755
+
756
+                    self.peerconnection.setLocalDescription(modifiedAnswer,
757
+                        function() {
758
+                            //console.log('modified setLocalDescription ok');
759
+                            $(document).trigger('setLocalDescription.jingle', [self.sid]);
760
+                        },
761
+                        function(error) {
762
+                            console.log('modified setLocalDescription failed');
763
+                        }
764
+                    );
765
+                },
766
+                function(error) {
767
+                    console.log('modified answer failed');
768
+                }
769
+            );
770
+        },
771
+        function(error) {
772
+            console.log('modify failed');
773
+        }
774
+    );
775
+};
776
+
777
+// SDP-based mute by going recvonly/sendrecv
778
+// FIXME: should probably black out the screen as well
779
+JingleSession.prototype.hardMuteVideo = function (muted) {
780
+    this.pendingop = muted ? 'mute' : 'unmute';
781
+    this.modifySources();
782
+
783
+    this.connection.jingle.localStream.getVideoTracks().forEach(function (track) {
784
+        track.enabled = !muted;
785
+    });
786
+};
787
+
788
+JingleSession.prototype.sendMute = function (muted, content) {
789
+    var info = $iq({to: this.peerjid,
790
+        type: 'set'})
791
+        .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
792
+            action: 'session-info',
793
+            initiator: this.initiator,
794
+            sid: this.sid });
795
+    info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
796
+    info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
797
+    if (content) {
798
+        info.attrs({'name': content});
799
+    }
800
+    this.connection.send(info);
801
+};
802
+
803
+JingleSession.prototype.sendRinging = function () {
804
+    var info = $iq({to: this.peerjid,
805
+        type: 'set'})
806
+        .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
807
+            action: 'session-info',
808
+            initiator: this.initiator,
809
+            sid: this.sid });
810
+    info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
811
+    this.connection.send(info);
812
+};
813
+
814
+JingleSession.prototype.getStats = function (interval) {
815
+    var self = this;
816
+    var recv = {audio: 0, video: 0};
817
+    var lost = {audio: 0, video: 0};
818
+    var lastrecv = {audio: 0, video: 0};
819
+    var lastlost = {audio: 0, video: 0};
820
+    var loss = {audio: 0, video: 0};
821
+    var delta = {audio: 0, video: 0};
822
+    this.statsinterval = window.setInterval(function () {
823
+        if (self && self.peerconnection && self.peerconnection.getStats) {
824
+            self.peerconnection.getStats(function (stats) {
825
+                var results = stats.result();
826
+                // TODO: there are so much statistics you can get from this..
827
+                for (var i = 0; i < results.length; ++i) {
828
+                    if (results[i].type == 'ssrc') {
829
+                        var packetsrecv = results[i].stat('packetsReceived');
830
+                        var packetslost = results[i].stat('packetsLost');
831
+                        if (packetsrecv && packetslost) {
832
+                            packetsrecv = parseInt(packetsrecv, 10);
833
+                            packetslost = parseInt(packetslost, 10);
834
+
835
+                            if (results[i].stat('googFrameRateReceived')) {
836
+                                lastlost.video = lost.video;
837
+                                lastrecv.video = recv.video;
838
+                                recv.video = packetsrecv;
839
+                                lost.video = packetslost;
840
+                            } else {
841
+                                lastlost.audio = lost.audio;
842
+                                lastrecv.audio = recv.audio;
843
+                                recv.audio = packetsrecv;
844
+                                lost.audio = packetslost;
845
+                            }
846
+                        }
847
+                    }
848
+                }
849
+                delta.audio = recv.audio - lastrecv.audio;
850
+                delta.video = recv.video - lastrecv.video;
851
+                loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
852
+                loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
853
+                $(document).trigger('packetloss.jingle', [self.sid, loss]);
854
+            });
855
+        }
856
+    }, interval || 3000);
857
+    return this.statsinterval;
858
+};

+ 0
- 2304
libs/strophejingle.bundle.js
文件差异内容过多而无法显示
查看文件


正在加载...
取消
保存