Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

strophe.jingle.session.js 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. /* jshint -W117 */
  2. // Jingle stuff
  3. JingleSession.prototype = Object.create(SessionBase.prototype);
  4. function JingleSession(me, sid, connection) {
  5. SessionBase.call(this, connection, sid);
  6. this.me = me;
  7. this.initiator = null;
  8. this.responder = null;
  9. this.isInitiator = null;
  10. this.peerjid = null;
  11. this.state = null;
  12. this.localSDP = null;
  13. this.remoteSDP = null;
  14. this.localStreams = [];
  15. this.relayedStreams = [];
  16. this.remoteStreams = [];
  17. this.startTime = null;
  18. this.stopTime = null;
  19. this.media_constraints = null;
  20. this.pc_constraints = null;
  21. this.ice_config = {};
  22. this.drip_container = [];
  23. this.usetrickle = true;
  24. this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
  25. this.usedrip = false; // dripping is sending trickle candidates not one-by-one
  26. this.hadstuncandidate = false;
  27. this.hadturncandidate = false;
  28. this.lasticecandidate = false;
  29. this.statsinterval = null;
  30. this.reason = null;
  31. this.wait = true;
  32. this.localStreamsSSRC = null;
  33. }
  34. JingleSession.prototype.initiate = function (peerjid, isInitiator) {
  35. var self = this;
  36. if (this.state !== null) {
  37. console.error('attempt to initiate on session ' + this.sid +
  38. 'in state ' + this.state);
  39. return;
  40. }
  41. this.isInitiator = isInitiator;
  42. this.state = 'pending';
  43. this.initiator = isInitiator ? this.me : peerjid;
  44. this.responder = !isInitiator ? this.me : peerjid;
  45. this.peerjid = peerjid;
  46. this.hadstuncandidate = false;
  47. this.hadturncandidate = false;
  48. this.lasticecandidate = false;
  49. this.peerconnection
  50. = new TraceablePeerConnection(
  51. this.connection.jingle.ice_config,
  52. this.connection.jingle.pc_constraints );
  53. this.peerconnection.onicecandidate = function (event) {
  54. self.sendIceCandidate(event.candidate);
  55. };
  56. this.peerconnection.onaddstream = function (event) {
  57. self.remoteStreams.push(event.stream);
  58. console.log("REMOTE STREAM ADDED: " + event.stream + " - " + event.stream.id);
  59. $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
  60. };
  61. this.peerconnection.onremovestream = function (event) {
  62. // Remove the stream from remoteStreams
  63. var streamIdx = self.remoteStreams.indexOf(event.stream);
  64. if(streamIdx !== -1){
  65. self.remoteStreams.splice(streamIdx, 1);
  66. }
  67. // FIXME: remotestreamremoved.jingle not defined anywhere(unused)
  68. $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
  69. };
  70. this.peerconnection.onsignalingstatechange = function (event) {
  71. if (!(self && self.peerconnection)) return;
  72. };
  73. this.peerconnection.oniceconnectionstatechange = function (event) {
  74. if (!(self && self.peerconnection)) return;
  75. switch (self.peerconnection.iceConnectionState) {
  76. case 'connected':
  77. this.startTime = new Date();
  78. break;
  79. case 'disconnected':
  80. this.stopTime = new Date();
  81. break;
  82. }
  83. $(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
  84. };
  85. // add any local and relayed stream
  86. this.localStreams.forEach(function(stream) {
  87. self.peerconnection.addStream(stream);
  88. });
  89. this.relayedStreams.forEach(function(stream) {
  90. self.peerconnection.addStream(stream);
  91. });
  92. };
  93. JingleSession.prototype.accept = function () {
  94. var self = this;
  95. this.state = 'active';
  96. var pranswer = this.peerconnection.localDescription;
  97. if (!pranswer || pranswer.type != 'pranswer') {
  98. return;
  99. }
  100. console.log('going from pranswer to answer');
  101. if (this.usetrickle) {
  102. // remove candidates already sent from session-accept
  103. var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
  104. for (var i = 0; i < lines.length; i++) {
  105. pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
  106. }
  107. }
  108. while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
  109. // FIXME: change any inactive to sendrecv or whatever they were originally
  110. pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
  111. }
  112. pranswer = simulcast.reverseTransformLocalDescription(pranswer);
  113. var prsdp = new SDP(pranswer.sdp);
  114. var accept = $iq({to: this.peerjid,
  115. type: 'set'})
  116. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  117. action: 'session-accept',
  118. initiator: this.initiator,
  119. responder: this.responder,
  120. sid: this.sid });
  121. prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
  122. var sdp = this.peerconnection.localDescription.sdp;
  123. while (SDPUtil.find_line(sdp, 'a=inactive')) {
  124. // FIXME: change any inactive to sendrecv or whatever they were originally
  125. sdp = sdp.replace('a=inactive', 'a=sendrecv');
  126. }
  127. this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
  128. function () {
  129. //console.log('setLocalDescription success');
  130. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  131. this.connection.sendIQ(accept,
  132. function () {
  133. var ack = {};
  134. ack.source = 'answer';
  135. $(document).trigger('ack.jingle', [self.sid, ack]);
  136. },
  137. function (stanza) {
  138. var error = ($(stanza).find('error').length) ? {
  139. code: $(stanza).find('error').attr('code'),
  140. reason: $(stanza).find('error :first')[0].tagName
  141. }:{};
  142. error.source = 'answer';
  143. $(document).trigger('error.jingle', [self.sid, error]);
  144. },
  145. 10000);
  146. },
  147. function (e) {
  148. console.error('setLocalDescription failed', e);
  149. }
  150. );
  151. };
  152. /**
  153. * Implements SessionBase.sendSSRCUpdate.
  154. */
  155. JingleSession.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isadd) {
  156. var self = this;
  157. console.log('tell', self.peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from' + self.me);
  158. if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){
  159. console.log("Too early to send updates");
  160. return;
  161. }
  162. this.sendSSRCUpdateIq(sdpMediaSsrcs, self.sid, self.initiator, self.peerjid, isadd);
  163. };
  164. JingleSession.prototype.terminate = function (reason) {
  165. this.state = 'ended';
  166. this.reason = reason;
  167. this.peerconnection.close();
  168. if (this.statsinterval !== null) {
  169. window.clearInterval(this.statsinterval);
  170. this.statsinterval = null;
  171. }
  172. };
  173. JingleSession.prototype.active = function () {
  174. return this.state == 'active';
  175. };
  176. JingleSession.prototype.sendIceCandidate = function (candidate) {
  177. var self = this;
  178. if (candidate && !this.lasticecandidate) {
  179. var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
  180. var jcand = SDPUtil.candidateToJingle(candidate.candidate);
  181. if (!(ice && jcand)) {
  182. console.error('failed to get ice && jcand');
  183. return;
  184. }
  185. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  186. if (jcand.type === 'srflx') {
  187. this.hadstuncandidate = true;
  188. } else if (jcand.type === 'relay') {
  189. this.hadturncandidate = true;
  190. }
  191. if (this.usetrickle) {
  192. if (this.usedrip) {
  193. if (this.drip_container.length === 0) {
  194. // start 20ms callout
  195. window.setTimeout(function () {
  196. if (self.drip_container.length === 0) return;
  197. self.sendIceCandidates(self.drip_container);
  198. self.drip_container = [];
  199. }, 20);
  200. }
  201. this.drip_container.push(candidate);
  202. return;
  203. } else {
  204. self.sendIceCandidate([candidate]);
  205. }
  206. }
  207. } else {
  208. //console.log('sendIceCandidate: last candidate.');
  209. if (!this.usetrickle) {
  210. //console.log('should send full offer now...');
  211. var init = $iq({to: this.peerjid,
  212. type: 'set'})
  213. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  214. action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
  215. initiator: this.initiator,
  216. sid: this.sid});
  217. this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
  218. var self = this;
  219. var sendJingle = function (ssrc) {
  220. if(!ssrc)
  221. ssrc = {};
  222. self.localSDP.toJingle(init, self.initiator == self.me ? 'initiator' : 'responder', ssrc);
  223. self.connection.sendIQ(init,
  224. function () {
  225. //console.log('session initiate ack');
  226. var ack = {};
  227. ack.source = 'offer';
  228. $(document).trigger('ack.jingle', [self.sid, ack]);
  229. },
  230. function (stanza) {
  231. self.state = 'error';
  232. self.peerconnection.close();
  233. var error = ($(stanza).find('error').length) ? {
  234. code: $(stanza).find('error').attr('code'),
  235. reason: $(stanza).find('error :first')[0].tagName,
  236. }:{};
  237. error.source = 'offer';
  238. $(document).trigger('error.jingle', [self.sid, error]);
  239. },
  240. 10000);
  241. }
  242. sendJingle();
  243. }
  244. this.lasticecandidate = true;
  245. console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
  246. console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
  247. if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
  248. $(document).trigger('nostuncandidates.jingle', [this.sid]);
  249. }
  250. }
  251. };
  252. JingleSession.prototype.sendIceCandidates = function (candidates) {
  253. console.log('sendIceCandidates', candidates);
  254. var cand = $iq({to: this.peerjid, type: 'set'})
  255. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  256. action: 'transport-info',
  257. initiator: this.initiator,
  258. sid: this.sid});
  259. for (var mid = 0; mid < this.localSDP.media.length; mid++) {
  260. var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
  261. var mline = SDPUtil.parse_mline(this.localSDP.media[mid].split('\r\n')[0]);
  262. if (cands.length > 0) {
  263. var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
  264. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  265. cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
  266. name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
  267. }).c('transport', ice);
  268. for (var i = 0; i < cands.length; i++) {
  269. cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
  270. }
  271. // add fingerprint
  272. if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
  273. var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
  274. tmp.required = true;
  275. cand.c(
  276. 'fingerprint',
  277. {xmlns: 'urn:xmpp:jingle:apps:dtls:0'})
  278. .t(tmp.fingerprint);
  279. delete tmp.fingerprint;
  280. cand.attrs(tmp);
  281. cand.up();
  282. }
  283. cand.up(); // transport
  284. cand.up(); // content
  285. }
  286. }
  287. // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
  288. //console.log('was this the last candidate', this.lasticecandidate);
  289. this.connection.sendIQ(cand,
  290. function () {
  291. var ack = {};
  292. ack.source = 'transportinfo';
  293. $(document).trigger('ack.jingle', [this.sid, ack]);
  294. },
  295. function (stanza) {
  296. var error = ($(stanza).find('error').length) ? {
  297. code: $(stanza).find('error').attr('code'),
  298. reason: $(stanza).find('error :first')[0].tagName,
  299. }:{};
  300. error.source = 'transportinfo';
  301. $(document).trigger('error.jingle', [this.sid, error]);
  302. },
  303. 10000);
  304. };
  305. JingleSession.prototype.sendOffer = function () {
  306. //console.log('sendOffer...');
  307. var self = this;
  308. this.peerconnection.createOffer(function (sdp) {
  309. self.createdOffer(sdp);
  310. },
  311. function (e) {
  312. console.error('createOffer failed', e);
  313. },
  314. this.media_constraints
  315. );
  316. };
  317. JingleSession.prototype.createdOffer = function (sdp) {
  318. //console.log('createdOffer', sdp);
  319. var self = this;
  320. this.localSDP = new SDP(sdp.sdp);
  321. //this.localSDP.mangle();
  322. var sendJingle = function () {
  323. var init = $iq({to: this.peerjid,
  324. type: 'set'})
  325. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  326. action: 'session-initiate',
  327. initiator: this.initiator,
  328. sid: this.sid});
  329. this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
  330. this.connection.sendIQ(init,
  331. function () {
  332. var ack = {};
  333. ack.source = 'offer';
  334. $(document).trigger('ack.jingle', [self.sid, ack]);
  335. },
  336. function (stanza) {
  337. self.state = 'error';
  338. self.peerconnection.close();
  339. var error = ($(stanza).find('error').length) ? {
  340. code: $(stanza).find('error').attr('code'),
  341. reason: $(stanza).find('error :first')[0].tagName,
  342. }:{};
  343. error.source = 'offer';
  344. $(document).trigger('error.jingle', [self.sid, error]);
  345. },
  346. 10000);
  347. }
  348. sdp.sdp = this.localSDP.raw;
  349. this.peerconnection.setLocalDescription(sdp,
  350. function () {
  351. if(this.usetrickle)
  352. {
  353. sendJingle();
  354. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  355. }
  356. else
  357. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  358. //console.log('setLocalDescription success');
  359. },
  360. function (e) {
  361. console.error('setLocalDescription failed', e);
  362. }
  363. );
  364. var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
  365. for (var i = 0; i < cands.length; i++) {
  366. var cand = SDPUtil.parse_icecandidate(cands[i]);
  367. if (cand.type == 'srflx') {
  368. this.hadstuncandidate = true;
  369. } else if (cand.type == 'relay') {
  370. this.hadturncandidate = true;
  371. }
  372. }
  373. };
  374. JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
  375. //console.log('setting remote description... ', desctype);
  376. this.remoteSDP = new SDP('');
  377. this.remoteSDP.fromJingle(elem);
  378. if (this.peerconnection.remoteDescription !== null) {
  379. console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
  380. if (this.peerconnection.remoteDescription.type == 'pranswer') {
  381. var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
  382. for (var i = 0; i < pranswer.media.length; i++) {
  383. // make sure we have ice ufrag and pwd
  384. if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
  385. if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
  386. this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
  387. } else {
  388. console.warn('no ice ufrag?');
  389. }
  390. if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
  391. this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
  392. } else {
  393. console.warn('no ice pwd?');
  394. }
  395. }
  396. // copy over candidates
  397. var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
  398. for (var j = 0; j < lines.length; j++) {
  399. this.remoteSDP.media[i] += lines[j] + '\r\n';
  400. }
  401. }
  402. this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
  403. }
  404. }
  405. var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
  406. this.peerconnection.setRemoteDescription(remotedesc,
  407. function () {
  408. //console.log('setRemoteDescription success');
  409. },
  410. function (e) {
  411. console.error('setRemoteDescription error', e);
  412. $(document).trigger('fatalError.jingle', [self, e]);
  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. // we need a complete SDP with ice-ufrag/ice-pwd in all parts
  463. // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
  464. // but it could be in the session part as well. since the code above constructs this sdp this can't happen however
  465. var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
  466. return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
  467. }).length == this.remoteSDP.media.length;
  468. if (iscomplete) {
  469. console.log('setting pranswer');
  470. try {
  471. this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
  472. function() {
  473. },
  474. function(e) {
  475. console.log('setRemoteDescription pranswer failed', e.toString());
  476. });
  477. } catch (e) {
  478. console.error('setting pranswer failed', e);
  479. }
  480. } else {
  481. //console.log('not yet setting pranswer');
  482. }
  483. }
  484. // operate on each content element
  485. elem.each(function () {
  486. // would love to deactivate this, but firefox still requires it
  487. var idx = -1;
  488. var i;
  489. for (i = 0; i < self.remoteSDP.media.length; i++) {
  490. if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  491. self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  492. idx = i;
  493. break;
  494. }
  495. }
  496. if (idx == -1) { // fall back to localdescription
  497. for (i = 0; i < self.localSDP.media.length; i++) {
  498. if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  499. self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  500. idx = i;
  501. break;
  502. }
  503. }
  504. }
  505. var name = $(this).attr('name');
  506. // TODO: check ice-pwd and ice-ufrag?
  507. $(this).find('transport>candidate').each(function () {
  508. var line, candidate;
  509. line = SDPUtil.candidateFromJingle(this);
  510. candidate = new RTCIceCandidate({sdpMLineIndex: idx,
  511. sdpMid: name,
  512. candidate: line});
  513. try {
  514. self.peerconnection.addIceCandidate(candidate);
  515. } catch (e) {
  516. console.error('addIceCandidate failed', e.toString(), line);
  517. }
  518. });
  519. });
  520. };
  521. JingleSession.prototype.sendAnswer = function (provisional) {
  522. //console.log('createAnswer', provisional);
  523. var self = this;
  524. this.peerconnection.createAnswer(
  525. function (sdp) {
  526. self.createdAnswer(sdp, provisional);
  527. },
  528. function (e) {
  529. console.error('createAnswer failed', e);
  530. },
  531. this.media_constraints
  532. );
  533. };
  534. JingleSession.prototype.createdAnswer = function (sdp, provisional) {
  535. //console.log('createAnswer callback');
  536. var self = this;
  537. this.localSDP = new SDP(sdp.sdp);
  538. //this.localSDP.mangle();
  539. this.usepranswer = provisional === true;
  540. if (this.usetrickle) {
  541. if (this.usepranswer) {
  542. sdp.type = 'pranswer';
  543. for (var i = 0; i < this.localSDP.media.length; i++) {
  544. this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
  545. }
  546. this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
  547. }
  548. }
  549. var self = this;
  550. var sendJingle = function (ssrcs) {
  551. var accept = $iq({to: self.peerjid,
  552. type: 'set'})
  553. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  554. action: 'session-accept',
  555. initiator: self.initiator,
  556. responder: self.responder,
  557. sid: self.sid });
  558. var publicLocalDesc = simulcast.reverseTransformLocalDescription(sdp);
  559. var publicLocalSDP = new SDP(publicLocalDesc.sdp);
  560. publicLocalSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs);
  561. this.connection.sendIQ(accept,
  562. function () {
  563. var ack = {};
  564. ack.source = 'answer';
  565. $(document).trigger('ack.jingle', [self.sid, ack]);
  566. },
  567. function (stanza) {
  568. var error = ($(stanza).find('error').length) ? {
  569. code: $(stanza).find('error').attr('code'),
  570. reason: $(stanza).find('error :first')[0].tagName,
  571. }:{};
  572. error.source = 'answer';
  573. $(document).trigger('error.jingle', [self.sid, error]);
  574. },
  575. 10000);
  576. }
  577. sdp.sdp = this.localSDP.raw;
  578. this.peerconnection.setLocalDescription(sdp,
  579. function () {
  580. //console.log('setLocalDescription success');
  581. if (self.usetrickle && !self.usepranswer) {
  582. sendJingle();
  583. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  584. }
  585. else
  586. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  587. },
  588. function (e) {
  589. console.error('setLocalDescription failed', e);
  590. }
  591. );
  592. var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
  593. for (var j = 0; j < cands.length; j++) {
  594. var cand = SDPUtil.parse_icecandidate(cands[j]);
  595. if (cand.type == 'srflx') {
  596. this.hadstuncandidate = true;
  597. } else if (cand.type == 'relay') {
  598. this.hadturncandidate = true;
  599. }
  600. }
  601. };
  602. JingleSession.prototype.sendTerminate = function (reason, text) {
  603. var self = this,
  604. term = $iq({to: this.peerjid,
  605. type: 'set'})
  606. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  607. action: 'session-terminate',
  608. initiator: this.initiator,
  609. sid: this.sid})
  610. .c('reason')
  611. .c(reason || 'success');
  612. if (text) {
  613. term.up().c('text').t(text);
  614. }
  615. this.connection.sendIQ(term,
  616. function () {
  617. self.peerconnection.close();
  618. self.peerconnection = null;
  619. self.terminate();
  620. var ack = {};
  621. ack.source = 'terminate';
  622. $(document).trigger('ack.jingle', [self.sid, ack]);
  623. },
  624. function (stanza) {
  625. var error = ($(stanza).find('error').length) ? {
  626. code: $(stanza).find('error').attr('code'),
  627. reason: $(stanza).find('error :first')[0].tagName,
  628. }:{};
  629. $(document).trigger('ack.jingle', [self.sid, error]);
  630. },
  631. 10000);
  632. if (this.statsinterval !== null) {
  633. window.clearInterval(this.statsinterval);
  634. this.statsinterval = null;
  635. }
  636. };
  637. JingleSession.prototype.sendMute = function (muted, content) {
  638. var info = $iq({to: this.peerjid,
  639. type: 'set'})
  640. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  641. action: 'session-info',
  642. initiator: this.initiator,
  643. sid: this.sid });
  644. info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
  645. info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
  646. if (content) {
  647. info.attrs({'name': content});
  648. }
  649. this.connection.send(info);
  650. };
  651. JingleSession.prototype.sendRinging = function () {
  652. var info = $iq({to: this.peerjid,
  653. type: 'set'})
  654. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  655. action: 'session-info',
  656. initiator: this.initiator,
  657. sid: this.sid });
  658. info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
  659. this.connection.send(info);
  660. };
  661. JingleSession.prototype.getStats = function (interval) {
  662. var self = this;
  663. var recv = {audio: 0, video: 0};
  664. var lost = {audio: 0, video: 0};
  665. var lastrecv = {audio: 0, video: 0};
  666. var lastlost = {audio: 0, video: 0};
  667. var loss = {audio: 0, video: 0};
  668. var delta = {audio: 0, video: 0};
  669. this.statsinterval = window.setInterval(function () {
  670. if (self && self.peerconnection && self.peerconnection.getStats) {
  671. self.peerconnection.getStats(function (stats) {
  672. var results = stats.result();
  673. // TODO: there are so much statistics you can get from this..
  674. for (var i = 0; i < results.length; ++i) {
  675. if (results[i].type == 'ssrc') {
  676. var packetsrecv = results[i].stat('packetsReceived');
  677. var packetslost = results[i].stat('packetsLost');
  678. if (packetsrecv && packetslost) {
  679. packetsrecv = parseInt(packetsrecv, 10);
  680. packetslost = parseInt(packetslost, 10);
  681. if (results[i].stat('googFrameRateReceived')) {
  682. lastlost.video = lost.video;
  683. lastrecv.video = recv.video;
  684. recv.video = packetsrecv;
  685. lost.video = packetslost;
  686. } else {
  687. lastlost.audio = lost.audio;
  688. lastrecv.audio = recv.audio;
  689. recv.audio = packetsrecv;
  690. lost.audio = packetslost;
  691. }
  692. }
  693. }
  694. }
  695. delta.audio = recv.audio - lastrecv.audio;
  696. delta.video = recv.video - lastrecv.video;
  697. loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
  698. loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
  699. $(document).trigger('packetloss.jingle', [self.sid, loss]);
  700. });
  701. }
  702. }, interval || 3000);
  703. return this.statsinterval;
  704. };