You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

strophe.jingle.session.js 40KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  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.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. * The indicator which determines whether the (local) video has been muted
  35. * in response to a user command in contrast to an automatic decision made
  36. * by the application logic.
  37. */
  38. this.videoMuteByUser = false;
  39. }
  40. JingleSession.prototype.initiate = function (peerjid, isInitiator) {
  41. var self = this;
  42. if (this.state !== null) {
  43. console.error('attempt to initiate on session ' + this.sid +
  44. 'in state ' + this.state);
  45. return;
  46. }
  47. this.isInitiator = isInitiator;
  48. this.state = 'pending';
  49. this.initiator = isInitiator ? this.me : peerjid;
  50. this.responder = !isInitiator ? this.me : peerjid;
  51. this.peerjid = peerjid;
  52. this.hadstuncandidate = false;
  53. this.hadturncandidate = false;
  54. this.lasticecandidate = false;
  55. this.peerconnection
  56. = new TraceablePeerConnection(
  57. this.connection.jingle.ice_config,
  58. this.connection.jingle.pc_constraints );
  59. this.peerconnection.onicecandidate = function (event) {
  60. self.sendIceCandidate(event.candidate);
  61. };
  62. this.peerconnection.onaddstream = function (event) {
  63. self.remoteStreams.push(event.stream);
  64. console.log("REMOTE STREAM ADDED: " + event.stream + " - " + event.stream.id);
  65. $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
  66. };
  67. this.peerconnection.onremovestream = function (event) {
  68. // Remove the stream from remoteStreams
  69. var streamIdx = self.remoteStreams.indexOf(event.stream);
  70. if(streamIdx !== -1){
  71. self.remoteStreams.splice(streamIdx, 1);
  72. }
  73. // FIXME: remotestreamremoved.jingle not defined anywhere(unused)
  74. $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
  75. };
  76. this.peerconnection.onsignalingstatechange = function (event) {
  77. if (!(self && self.peerconnection)) return;
  78. };
  79. this.peerconnection.oniceconnectionstatechange = function (event) {
  80. if (!(self && self.peerconnection)) return;
  81. switch (self.peerconnection.iceConnectionState) {
  82. case 'connected':
  83. this.startTime = new Date();
  84. break;
  85. case 'disconnected':
  86. this.stopTime = new Date();
  87. break;
  88. }
  89. $(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
  90. };
  91. // add any local and relayed stream
  92. this.localStreams.forEach(function(stream) {
  93. self.peerconnection.addStream(stream);
  94. });
  95. this.relayedStreams.forEach(function(stream) {
  96. self.peerconnection.addStream(stream);
  97. });
  98. };
  99. JingleSession.prototype.accept = function () {
  100. var self = this;
  101. this.state = 'active';
  102. var pranswer = this.peerconnection.localDescription;
  103. if (!pranswer || pranswer.type != 'pranswer') {
  104. return;
  105. }
  106. console.log('going from pranswer to answer');
  107. if (this.usetrickle) {
  108. // remove candidates already sent from session-accept
  109. var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
  110. for (var i = 0; i < lines.length; i++) {
  111. pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
  112. }
  113. }
  114. while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
  115. // FIXME: change any inactive to sendrecv or whatever they were originally
  116. pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
  117. }
  118. pranswer = simulcast.reverseTransformLocalDescription(pranswer);
  119. var prsdp = new SDP(pranswer.sdp);
  120. var accept = $iq({to: this.peerjid,
  121. type: 'set'})
  122. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  123. action: 'session-accept',
  124. initiator: this.initiator,
  125. responder: this.responder,
  126. sid: this.sid });
  127. prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
  128. var sdp = this.peerconnection.localDescription.sdp;
  129. while (SDPUtil.find_line(sdp, 'a=inactive')) {
  130. // FIXME: change any inactive to sendrecv or whatever they were originally
  131. sdp = sdp.replace('a=inactive', 'a=sendrecv');
  132. }
  133. this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
  134. function () {
  135. //console.log('setLocalDescription success');
  136. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  137. this.connection.sendIQ(accept,
  138. function () {
  139. var ack = {};
  140. ack.source = 'answer';
  141. $(document).trigger('ack.jingle', [self.sid, ack]);
  142. },
  143. function (stanza) {
  144. var error = ($(stanza).find('error').length) ? {
  145. code: $(stanza).find('error').attr('code'),
  146. reason: $(stanza).find('error :first')[0].tagName
  147. }:{};
  148. error.source = 'answer';
  149. $(document).trigger('error.jingle', [self.sid, error]);
  150. },
  151. 10000);
  152. },
  153. function (e) {
  154. console.error('setLocalDescription failed', e);
  155. }
  156. );
  157. };
  158. JingleSession.prototype.terminate = function (reason) {
  159. this.state = 'ended';
  160. this.reason = reason;
  161. this.peerconnection.close();
  162. if (this.statsinterval !== null) {
  163. window.clearInterval(this.statsinterval);
  164. this.statsinterval = null;
  165. }
  166. };
  167. JingleSession.prototype.active = function () {
  168. return this.state == 'active';
  169. };
  170. JingleSession.prototype.sendIceCandidate = function (candidate) {
  171. var self = this;
  172. if (candidate && !this.lasticecandidate) {
  173. var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
  174. var jcand = SDPUtil.candidateToJingle(candidate.candidate);
  175. if (!(ice && jcand)) {
  176. console.error('failed to get ice && jcand');
  177. return;
  178. }
  179. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  180. if (jcand.type === 'srflx') {
  181. this.hadstuncandidate = true;
  182. } else if (jcand.type === 'relay') {
  183. this.hadturncandidate = true;
  184. }
  185. if (this.usetrickle) {
  186. if (this.usedrip) {
  187. if (this.drip_container.length === 0) {
  188. // start 20ms callout
  189. window.setTimeout(function () {
  190. if (self.drip_container.length === 0) return;
  191. self.sendIceCandidates(self.drip_container);
  192. self.drip_container = [];
  193. }, 20);
  194. }
  195. this.drip_container.push(candidate);
  196. return;
  197. } else {
  198. self.sendIceCandidate([candidate]);
  199. }
  200. }
  201. } else {
  202. //console.log('sendIceCandidate: last candidate.');
  203. if (!this.usetrickle) {
  204. //console.log('should send full offer now...');
  205. var init = $iq({to: this.peerjid,
  206. type: 'set'})
  207. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  208. action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
  209. initiator: this.initiator,
  210. sid: this.sid});
  211. this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
  212. var self = this;
  213. var sendJingle = function (ssrc) {
  214. if(!ssrc)
  215. ssrc = {};
  216. self.localSDP.toJingle(init, self.initiator == self.me ? 'initiator' : 'responder', ssrc);
  217. self.connection.sendIQ(init,
  218. function () {
  219. //console.log('session initiate ack');
  220. var ack = {};
  221. ack.source = 'offer';
  222. $(document).trigger('ack.jingle', [self.sid, ack]);
  223. },
  224. function (stanza) {
  225. self.state = 'error';
  226. self.peerconnection.close();
  227. var error = ($(stanza).find('error').length) ? {
  228. code: $(stanza).find('error').attr('code'),
  229. reason: $(stanza).find('error :first')[0].tagName,
  230. }:{};
  231. error.source = 'offer';
  232. $(document).trigger('error.jingle', [self.sid, error]);
  233. },
  234. 10000);
  235. }
  236. sendJingle();
  237. }
  238. this.lasticecandidate = true;
  239. console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
  240. console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
  241. if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
  242. $(document).trigger('nostuncandidates.jingle', [this.sid]);
  243. }
  244. }
  245. };
  246. JingleSession.prototype.sendIceCandidates = function (candidates) {
  247. console.log('sendIceCandidates', candidates);
  248. var cand = $iq({to: this.peerjid, type: 'set'})
  249. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  250. action: 'transport-info',
  251. initiator: this.initiator,
  252. sid: this.sid});
  253. for (var mid = 0; mid < this.localSDP.media.length; mid++) {
  254. var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
  255. var mline = SDPUtil.parse_mline(this.localSDP.media[mid].split('\r\n')[0]);
  256. if (cands.length > 0) {
  257. var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
  258. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  259. cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
  260. name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
  261. }).c('transport', ice);
  262. for (var i = 0; i < cands.length; i++) {
  263. cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
  264. }
  265. // add fingerprint
  266. if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
  267. var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
  268. tmp.required = true;
  269. cand.c(
  270. 'fingerprint',
  271. {xmlns: 'urn:xmpp:jingle:apps:dtls:0'})
  272. .t(tmp.fingerprint);
  273. delete tmp.fingerprint;
  274. cand.attrs(tmp);
  275. cand.up();
  276. }
  277. cand.up(); // transport
  278. cand.up(); // content
  279. }
  280. }
  281. // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
  282. //console.log('was this the last candidate', this.lasticecandidate);
  283. this.connection.sendIQ(cand,
  284. function () {
  285. var ack = {};
  286. ack.source = 'transportinfo';
  287. $(document).trigger('ack.jingle', [this.sid, ack]);
  288. },
  289. function (stanza) {
  290. var error = ($(stanza).find('error').length) ? {
  291. code: $(stanza).find('error').attr('code'),
  292. reason: $(stanza).find('error :first')[0].tagName,
  293. }:{};
  294. error.source = 'transportinfo';
  295. $(document).trigger('error.jingle', [this.sid, error]);
  296. },
  297. 10000);
  298. };
  299. JingleSession.prototype.sendOffer = function () {
  300. //console.log('sendOffer...');
  301. var self = this;
  302. this.peerconnection.createOffer(function (sdp) {
  303. self.createdOffer(sdp);
  304. },
  305. function (e) {
  306. console.error('createOffer failed', e);
  307. },
  308. this.media_constraints
  309. );
  310. };
  311. JingleSession.prototype.createdOffer = function (sdp) {
  312. //console.log('createdOffer', sdp);
  313. var self = this;
  314. this.localSDP = new SDP(sdp.sdp);
  315. //this.localSDP.mangle();
  316. var sendJingle = function () {
  317. var init = $iq({to: this.peerjid,
  318. type: 'set'})
  319. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  320. action: 'session-initiate',
  321. initiator: this.initiator,
  322. sid: this.sid});
  323. this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
  324. this.connection.sendIQ(init,
  325. function () {
  326. var ack = {};
  327. ack.source = 'offer';
  328. $(document).trigger('ack.jingle', [self.sid, ack]);
  329. },
  330. function (stanza) {
  331. self.state = 'error';
  332. self.peerconnection.close();
  333. var error = ($(stanza).find('error').length) ? {
  334. code: $(stanza).find('error').attr('code'),
  335. reason: $(stanza).find('error :first')[0].tagName,
  336. }:{};
  337. error.source = 'offer';
  338. $(document).trigger('error.jingle', [self.sid, error]);
  339. },
  340. 10000);
  341. }
  342. sdp.sdp = this.localSDP.raw;
  343. this.peerconnection.setLocalDescription(sdp,
  344. function () {
  345. if(this.usetrickle)
  346. {
  347. sendJingle();
  348. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  349. }
  350. else
  351. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  352. //console.log('setLocalDescription success');
  353. },
  354. function (e) {
  355. console.error('setLocalDescription failed', e);
  356. }
  357. );
  358. var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
  359. for (var i = 0; i < cands.length; i++) {
  360. var cand = SDPUtil.parse_icecandidate(cands[i]);
  361. if (cand.type == 'srflx') {
  362. this.hadstuncandidate = true;
  363. } else if (cand.type == 'relay') {
  364. this.hadturncandidate = true;
  365. }
  366. }
  367. };
  368. JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
  369. //console.log('setting remote description... ', desctype);
  370. this.remoteSDP = new SDP('');
  371. this.remoteSDP.fromJingle(elem);
  372. if (this.peerconnection.remoteDescription !== null) {
  373. console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
  374. if (this.peerconnection.remoteDescription.type == 'pranswer') {
  375. var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
  376. for (var i = 0; i < pranswer.media.length; i++) {
  377. // make sure we have ice ufrag and pwd
  378. if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
  379. if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
  380. this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
  381. } else {
  382. console.warn('no ice ufrag?');
  383. }
  384. if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
  385. this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
  386. } else {
  387. console.warn('no ice pwd?');
  388. }
  389. }
  390. // copy over candidates
  391. var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
  392. for (var j = 0; j < lines.length; j++) {
  393. this.remoteSDP.media[i] += lines[j] + '\r\n';
  394. }
  395. }
  396. this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
  397. }
  398. }
  399. var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
  400. this.peerconnection.setRemoteDescription(remotedesc,
  401. function () {
  402. //console.log('setRemoteDescription success');
  403. },
  404. function (e) {
  405. console.error('setRemoteDescription error', e);
  406. $(document).trigger('fatalError.jingle', [self, e]);
  407. }
  408. );
  409. };
  410. JingleSession.prototype.addIceCandidate = function (elem) {
  411. var self = this;
  412. if (this.peerconnection.signalingState == 'closed') {
  413. return;
  414. }
  415. if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
  416. console.log('trickle ice candidate arriving before session accept...');
  417. // create a PRANSWER for setRemoteDescription
  418. if (!this.remoteSDP) {
  419. var cobbled = 'v=0\r\n' +
  420. 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
  421. 's=-\r\n' +
  422. 't=0 0\r\n';
  423. // first, take some things from the local description
  424. for (var i = 0; i < this.localSDP.media.length; i++) {
  425. cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
  426. cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
  427. if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
  428. cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
  429. }
  430. cobbled += 'a=inactive\r\n';
  431. }
  432. this.remoteSDP = new SDP(cobbled);
  433. }
  434. // then add things like ice and dtls from remote candidate
  435. elem.each(function () {
  436. for (var i = 0; i < self.remoteSDP.media.length; i++) {
  437. if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  438. self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  439. if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
  440. var tmp = $(this).find('transport');
  441. self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  442. self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  443. tmp = $(this).find('transport>fingerprint');
  444. if (tmp.length) {
  445. self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  446. } else {
  447. console.log('no dtls fingerprint (webrtc issue #1718?)');
  448. self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
  449. }
  450. break;
  451. }
  452. }
  453. }
  454. });
  455. this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
  456. // we need a complete SDP with ice-ufrag/ice-pwd in all parts
  457. // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
  458. // but it could be in the session part as well. since the code above constructs this sdp this can't happen however
  459. var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
  460. return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
  461. }).length == this.remoteSDP.media.length;
  462. if (iscomplete) {
  463. console.log('setting pranswer');
  464. try {
  465. this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
  466. function() {
  467. },
  468. function(e) {
  469. console.log('setRemoteDescription pranswer failed', e.toString());
  470. });
  471. } catch (e) {
  472. console.error('setting pranswer failed', e);
  473. }
  474. } else {
  475. //console.log('not yet setting pranswer');
  476. }
  477. }
  478. // operate on each content element
  479. elem.each(function () {
  480. // would love to deactivate this, but firefox still requires it
  481. var idx = -1;
  482. var i;
  483. for (i = 0; i < self.remoteSDP.media.length; i++) {
  484. if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  485. self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  486. idx = i;
  487. break;
  488. }
  489. }
  490. if (idx == -1) { // fall back to localdescription
  491. for (i = 0; i < self.localSDP.media.length; i++) {
  492. if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  493. self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  494. idx = i;
  495. break;
  496. }
  497. }
  498. }
  499. var name = $(this).attr('name');
  500. // TODO: check ice-pwd and ice-ufrag?
  501. $(this).find('transport>candidate').each(function () {
  502. var line, candidate;
  503. line = SDPUtil.candidateFromJingle(this);
  504. candidate = new RTCIceCandidate({sdpMLineIndex: idx,
  505. sdpMid: name,
  506. candidate: line});
  507. try {
  508. self.peerconnection.addIceCandidate(candidate);
  509. } catch (e) {
  510. console.error('addIceCandidate failed', e.toString(), line);
  511. }
  512. });
  513. });
  514. };
  515. JingleSession.prototype.sendAnswer = function (provisional) {
  516. //console.log('createAnswer', provisional);
  517. var self = this;
  518. this.peerconnection.createAnswer(
  519. function (sdp) {
  520. self.createdAnswer(sdp, provisional);
  521. },
  522. function (e) {
  523. console.error('createAnswer failed', e);
  524. },
  525. this.media_constraints
  526. );
  527. };
  528. JingleSession.prototype.createdAnswer = function (sdp, provisional) {
  529. //console.log('createAnswer callback');
  530. var self = this;
  531. this.localSDP = new SDP(sdp.sdp);
  532. //this.localSDP.mangle();
  533. this.usepranswer = provisional === true;
  534. if (this.usetrickle) {
  535. if (this.usepranswer) {
  536. sdp.type = 'pranswer';
  537. for (var i = 0; i < this.localSDP.media.length; i++) {
  538. this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
  539. }
  540. this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
  541. }
  542. }
  543. var self = this;
  544. var sendJingle = function (ssrcs) {
  545. var accept = $iq({to: self.peerjid,
  546. type: 'set'})
  547. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  548. action: 'session-accept',
  549. initiator: self.initiator,
  550. responder: self.responder,
  551. sid: self.sid });
  552. var publicLocalDesc = simulcast.reverseTransformLocalDescription(sdp);
  553. var publicLocalSDP = new SDP(publicLocalDesc.sdp);
  554. publicLocalSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs);
  555. this.connection.sendIQ(accept,
  556. function () {
  557. var ack = {};
  558. ack.source = 'answer';
  559. $(document).trigger('ack.jingle', [self.sid, ack]);
  560. },
  561. function (stanza) {
  562. var error = ($(stanza).find('error').length) ? {
  563. code: $(stanza).find('error').attr('code'),
  564. reason: $(stanza).find('error :first')[0].tagName,
  565. }:{};
  566. error.source = 'answer';
  567. $(document).trigger('error.jingle', [self.sid, error]);
  568. },
  569. 10000);
  570. }
  571. sdp.sdp = this.localSDP.raw;
  572. this.peerconnection.setLocalDescription(sdp,
  573. function () {
  574. //console.log('setLocalDescription success');
  575. if (self.usetrickle && !self.usepranswer) {
  576. sendJingle();
  577. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  578. }
  579. else
  580. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  581. },
  582. function (e) {
  583. console.error('setLocalDescription failed', e);
  584. }
  585. );
  586. var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
  587. for (var j = 0; j < cands.length; j++) {
  588. var cand = SDPUtil.parse_icecandidate(cands[j]);
  589. if (cand.type == 'srflx') {
  590. this.hadstuncandidate = true;
  591. } else if (cand.type == 'relay') {
  592. this.hadturncandidate = true;
  593. }
  594. }
  595. };
  596. JingleSession.prototype.sendTerminate = function (reason, text) {
  597. var self = this,
  598. term = $iq({to: this.peerjid,
  599. type: 'set'})
  600. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  601. action: 'session-terminate',
  602. initiator: this.initiator,
  603. sid: this.sid})
  604. .c('reason')
  605. .c(reason || 'success');
  606. if (text) {
  607. term.up().c('text').t(text);
  608. }
  609. this.connection.sendIQ(term,
  610. function () {
  611. self.peerconnection.close();
  612. self.peerconnection = null;
  613. self.terminate();
  614. var ack = {};
  615. ack.source = 'terminate';
  616. $(document).trigger('ack.jingle', [self.sid, ack]);
  617. },
  618. function (stanza) {
  619. var error = ($(stanza).find('error').length) ? {
  620. code: $(stanza).find('error').attr('code'),
  621. reason: $(stanza).find('error :first')[0].tagName,
  622. }:{};
  623. $(document).trigger('ack.jingle', [self.sid, error]);
  624. },
  625. 10000);
  626. if (this.statsinterval !== null) {
  627. window.clearInterval(this.statsinterval);
  628. this.statsinterval = null;
  629. }
  630. };
  631. JingleSession.prototype.addSource = function (elem, fromJid) {
  632. var self = this;
  633. // FIXME: dirty waiting
  634. if (!this.peerconnection.localDescription)
  635. {
  636. console.warn("addSource - localDescription not ready yet")
  637. setTimeout(function()
  638. {
  639. self.addSource(elem, fromJid);
  640. },
  641. 200
  642. );
  643. return;
  644. }
  645. this.peerconnection.addSource(elem);
  646. this.modifySources();
  647. };
  648. JingleSession.prototype.removeSource = function (elem, fromJid) {
  649. var self = this;
  650. // FIXME: dirty waiting
  651. if (!this.peerconnection.localDescription)
  652. {
  653. console.warn("removeSource - localDescription not ready yet")
  654. setTimeout(function()
  655. {
  656. self.removeSource(elem, fromJid);
  657. },
  658. 200
  659. );
  660. return;
  661. }
  662. this.peerconnection.removeSource(elem);
  663. this.modifySources();
  664. };
  665. JingleSession.prototype.modifySources = function (successCallback) {
  666. var self = this;
  667. if(this.peerconnection)
  668. this.peerconnection.modifySources(function(){
  669. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  670. if(successCallback) {
  671. successCallback();
  672. }
  673. });
  674. };
  675. /**
  676. * Switches video streams.
  677. * @param new_stream new stream that will be used as video of this session.
  678. * @param oldStream old video stream of this session.
  679. * @param success_callback callback executed after successful stream switch.
  680. */
  681. JingleSession.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
  682. var self = this;
  683. // Stop the stream to trigger onended event for old stream
  684. oldStream.stop();
  685. // Remember SDP to figure out added/removed SSRCs
  686. var oldSdp = null;
  687. if(self.peerconnection) {
  688. if(self.peerconnection.localDescription) {
  689. oldSdp = new SDP(self.peerconnection.localDescription.sdp);
  690. }
  691. self.peerconnection.removeStream(oldStream, true);
  692. self.peerconnection.addStream(new_stream);
  693. }
  694. self.connection.jingle.localVideo = new_stream;
  695. self.connection.jingle.localStreams = [];
  696. //in firefox we have only one stream object
  697. if(self.connection.jingle.localAudio != self.connection.jingle.localVideo)
  698. self.connection.jingle.localStreams.push(self.connection.jingle.localAudio);
  699. self.connection.jingle.localStreams.push(self.connection.jingle.localVideo);
  700. // Conference is not active
  701. if(!oldSdp || !self.peerconnection) {
  702. success_callback();
  703. return;
  704. }
  705. self.peerconnection.switchstreams = true;
  706. self.modifySources(function() {
  707. console.log('modify sources done');
  708. success_callback();
  709. var newSdp = new SDP(self.peerconnection.localDescription.sdp);
  710. console.log("SDPs", oldSdp, newSdp);
  711. self.notifyMySSRCUpdate(oldSdp, newSdp);
  712. });
  713. };
  714. /**
  715. * Figures out added/removed ssrcs and send update IQs.
  716. * @param old_sdp SDP object for old description.
  717. * @param new_sdp SDP object for new description.
  718. */
  719. JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
  720. var old_media = old_sdp.getMediaSsrcMap();
  721. var new_media = new_sdp.getMediaSsrcMap();
  722. //console.log("old/new medias: ", old_media, new_media);
  723. var toAdd = old_sdp.getNewMedia(new_sdp);
  724. var toRemove = new_sdp.getNewMedia(old_sdp);
  725. //console.log("to add", toAdd);
  726. //console.log("to remove", toRemove);
  727. if(Object.keys(toRemove).length > 0){
  728. this.sendSSRCUpdate(toRemove, null, false);
  729. }
  730. if(Object.keys(toAdd).length > 0){
  731. this.sendSSRCUpdate(toAdd, null, true);
  732. }
  733. };
  734. /**
  735. * Empty method that does nothing by default. It should send SSRC update IQs to session participants.
  736. * @param sdpMediaSsrcs array of
  737. * @param fromJid
  738. * @param isAdd
  739. */
  740. JingleSession.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isAdd) {
  741. var self = this;
  742. console.log('tell', self.peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from' + self.me);
  743. if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){
  744. console.log("Too early to send updates");
  745. return;
  746. }
  747. this.sendSSRCUpdateIq(sdpMediaSsrcs, self.sid, self.initiator, self.peerjid, isadd);
  748. }
  749. /**
  750. * Sends SSRC update IQ.
  751. * @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
  752. * @param sid session identifier that will be put into the IQ.
  753. * @param initiator initiator identifier.
  754. * @param toJid destination Jid
  755. * @param isAdd indicates if this is remove or add operation.
  756. */
  757. JingleSession.prototype.sendSSRCUpdateIq = function(sdpMediaSsrcs, sid, initiator, toJid, isAdd) {
  758. var self = this;
  759. var modify = $iq({to: toJid, type: 'set'})
  760. .c('jingle', {
  761. xmlns: 'urn:xmpp:jingle:1',
  762. action: isAdd ? 'source-add' : 'source-remove',
  763. initiator: initiator,
  764. sid: sid
  765. }
  766. );
  767. // FIXME: only announce video ssrcs since we mix audio and dont need
  768. // the audio ssrcs therefore
  769. var modified = false;
  770. Object.keys(sdpMediaSsrcs).forEach(function(channelNum){
  771. modified = true;
  772. var channel = sdpMediaSsrcs[channelNum];
  773. modify.c('content', {name: channel.mediaType});
  774. modify.c('description', {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: channel.mediaType});
  775. // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
  776. // generate sources from lines
  777. Object.keys(channel.ssrcs).forEach(function(ssrcNum) {
  778. var mediaSsrc = channel.ssrcs[ssrcNum];
  779. modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
  780. modify.attrs({ssrc: mediaSsrc.ssrc});
  781. // iterate over ssrc lines
  782. mediaSsrc.lines.forEach(function (line) {
  783. var idx = line.indexOf(' ');
  784. var kv = line.substr(idx + 1);
  785. modify.c('parameter');
  786. if (kv.indexOf(':') == -1) {
  787. modify.attrs({ name: kv });
  788. } else {
  789. modify.attrs({ name: kv.split(':', 2)[0] });
  790. modify.attrs({ value: kv.split(':', 2)[1] });
  791. }
  792. modify.up(); // end of parameter
  793. });
  794. modify.up(); // end of source
  795. });
  796. // generate source groups from lines
  797. channel.ssrcGroups.forEach(function(ssrcGroup) {
  798. if (ssrcGroup.ssrcs.length != 0) {
  799. modify.c('ssrc-group', {
  800. semantics: ssrcGroup.semantics,
  801. xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
  802. });
  803. ssrcGroup.ssrcs.forEach(function (ssrc) {
  804. modify.c('source', { ssrc: ssrc })
  805. .up(); // end of source
  806. });
  807. modify.up(); // end of ssrc-group
  808. }
  809. });
  810. modify.up(); // end of description
  811. modify.up(); // end of content
  812. });
  813. if (modified) {
  814. self.connection.sendIQ(modify,
  815. function (res) {
  816. console.info('got modify result', res);
  817. },
  818. function (err) {
  819. console.error('got modify error', err);
  820. }
  821. );
  822. } else {
  823. console.log('modification not necessary');
  824. }
  825. };
  826. /**
  827. * Determines whether the (local) video is mute i.e. all video tracks are
  828. * disabled.
  829. *
  830. * @return <tt>true</tt> if the (local) video is mute i.e. all video tracks are
  831. * disabled; otherwise, <tt>false</tt>
  832. */
  833. JingleSession.prototype.isVideoMute = function () {
  834. var tracks = connection.jingle.localVideo.getVideoTracks();
  835. var mute = true;
  836. for (var i = 0; i < tracks.length; ++i) {
  837. if (tracks[i].enabled) {
  838. mute = false;
  839. break;
  840. }
  841. }
  842. return mute;
  843. };
  844. /**
  845. * Mutes/unmutes the (local) video i.e. enables/disables all video tracks.
  846. *
  847. * @param mute <tt>true</tt> to mute the (local) video i.e. to disable all video
  848. * tracks; otherwise, <tt>false</tt>
  849. * @param callback a function to be invoked with <tt>mute</tt> after all video
  850. * tracks have been enabled/disabled. The function may, optionally, return
  851. * another function which is to be invoked after the whole mute/unmute operation
  852. * has completed successfully.
  853. * @param options an object which specifies optional arguments such as the
  854. * <tt>boolean</tt> key <tt>byUser</tt> with default value <tt>true</tt> which
  855. * specifies whether the method was initiated in response to a user command (in
  856. * contrast to an automatic decision made by the application logic)
  857. */
  858. JingleSession.prototype.setVideoMute = function (mute, callback, options) {
  859. var byUser;
  860. if (options) {
  861. byUser = options.byUser;
  862. if (typeof byUser === 'undefined') {
  863. byUser = true;
  864. }
  865. } else {
  866. byUser = true;
  867. }
  868. // The user's command to mute the (local) video takes precedence over any
  869. // automatic decision made by the application logic.
  870. if (byUser) {
  871. this.videoMuteByUser = mute;
  872. } else if (this.videoMuteByUser) {
  873. return;
  874. }
  875. if (mute == this.isVideoMute())
  876. {
  877. // Even if no change occurs, the specified callback is to be executed.
  878. // The specified callback may, optionally, return a successCallback
  879. // which is to be executed as well.
  880. var successCallback = callback(mute);
  881. if (successCallback) {
  882. successCallback();
  883. }
  884. } else {
  885. var tracks = connection.jingle.localVideo.getVideoTracks();
  886. for (var i = 0; i < tracks.length; ++i) {
  887. tracks[i].enabled = !mute;
  888. }
  889. if (this.peerconnection) {
  890. this.peerconnection.hardMuteVideo(mute);
  891. }
  892. this.modifySources(callback(mute));
  893. }
  894. };
  895. // SDP-based mute by going recvonly/sendrecv
  896. // FIXME: should probably black out the screen as well
  897. JingleSession.prototype.toggleVideoMute = function (callback) {
  898. setVideoMute(isVideoMute(), callback);
  899. };
  900. JingleSession.prototype.sendMute = function (muted, content) {
  901. var info = $iq({to: this.peerjid,
  902. type: 'set'})
  903. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  904. action: 'session-info',
  905. initiator: this.initiator,
  906. sid: this.sid });
  907. info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
  908. info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
  909. if (content) {
  910. info.attrs({'name': content});
  911. }
  912. this.connection.send(info);
  913. };
  914. JingleSession.prototype.sendRinging = function () {
  915. var info = $iq({to: this.peerjid,
  916. type: 'set'})
  917. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  918. action: 'session-info',
  919. initiator: this.initiator,
  920. sid: this.sid });
  921. info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
  922. this.connection.send(info);
  923. };
  924. JingleSession.prototype.getStats = function (interval) {
  925. var self = this;
  926. var recv = {audio: 0, video: 0};
  927. var lost = {audio: 0, video: 0};
  928. var lastrecv = {audio: 0, video: 0};
  929. var lastlost = {audio: 0, video: 0};
  930. var loss = {audio: 0, video: 0};
  931. var delta = {audio: 0, video: 0};
  932. this.statsinterval = window.setInterval(function () {
  933. if (self && self.peerconnection && self.peerconnection.getStats) {
  934. self.peerconnection.getStats(function (stats) {
  935. var results = stats.result();
  936. // TODO: there are so much statistics you can get from this..
  937. for (var i = 0; i < results.length; ++i) {
  938. if (results[i].type == 'ssrc') {
  939. var packetsrecv = results[i].stat('packetsReceived');
  940. var packetslost = results[i].stat('packetsLost');
  941. if (packetsrecv && packetslost) {
  942. packetsrecv = parseInt(packetsrecv, 10);
  943. packetslost = parseInt(packetslost, 10);
  944. if (results[i].stat('googFrameRateReceived')) {
  945. lastlost.video = lost.video;
  946. lastrecv.video = recv.video;
  947. recv.video = packetsrecv;
  948. lost.video = packetslost;
  949. } else {
  950. lastlost.audio = lost.audio;
  951. lastrecv.audio = recv.audio;
  952. recv.audio = packetsrecv;
  953. lost.audio = packetslost;
  954. }
  955. }
  956. }
  957. }
  958. delta.audio = recv.audio - lastrecv.audio;
  959. delta.video = recv.video - lastrecv.video;
  960. loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
  961. loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
  962. $(document).trigger('packetloss.jingle', [self.sid, loss]);
  963. });
  964. }
  965. }, interval || 3000);
  966. return this.statsinterval;
  967. };