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.

JingleSession.js 53KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393
  1. /* jshint -W117 */
  2. var TraceablePeerConnection = require("./TraceablePeerConnection");
  3. var SDPDiffer = require("./SDPDiffer");
  4. var SDPUtil = require("./SDPUtil");
  5. var SDP = require("./SDP");
  6. // Jingle stuff
  7. function JingleSession(me, sid, connection, service) {
  8. this.me = me;
  9. this.sid = sid;
  10. this.connection = connection;
  11. this.initiator = null;
  12. this.responder = null;
  13. this.isInitiator = null;
  14. this.peerjid = null;
  15. this.state = null;
  16. this.localSDP = null;
  17. this.remoteSDP = null;
  18. this.relayedStreams = [];
  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. this.service = service;
  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. this.hadstuncandidate = false;
  30. this.hadturncandidate = false;
  31. this.lasticecandidate = false;
  32. this.statsinterval = null;
  33. this.reason = null;
  34. this.addssrc = [];
  35. this.removessrc = [];
  36. this.pendingop = null;
  37. this.switchstreams = false;
  38. this.wait = true;
  39. this.localStreamsSSRC = null;
  40. /**
  41. * The indicator which determines whether the (local) video has been muted
  42. * in response to a user command in contrast to an automatic decision made
  43. * by the application logic.
  44. */
  45. this.videoMuteByUser = false;
  46. }
  47. //TODO: this array must be removed when firefox implement multistream support
  48. JingleSession.notReceivedSSRCs = [];
  49. JingleSession.prototype.initiate = function (peerjid, isInitiator) {
  50. var self = this;
  51. if (this.state !== null) {
  52. console.error('attempt to initiate on session ' + this.sid +
  53. 'in state ' + this.state);
  54. return;
  55. }
  56. this.isInitiator = isInitiator;
  57. this.state = 'pending';
  58. this.initiator = isInitiator ? this.me : peerjid;
  59. this.responder = !isInitiator ? this.me : peerjid;
  60. this.peerjid = peerjid;
  61. this.hadstuncandidate = false;
  62. this.hadturncandidate = false;
  63. this.lasticecandidate = false;
  64. this.peerconnection
  65. = new TraceablePeerConnection(
  66. this.connection.jingle.ice_config,
  67. this.connection.jingle.pc_constraints );
  68. this.peerconnection.onicecandidate = function (event) {
  69. self.sendIceCandidate(event.candidate);
  70. };
  71. this.peerconnection.onaddstream = function (event) {
  72. console.log("REMOTE STREAM ADDED: " + event.stream + " - " + event.stream.id);
  73. self.remoteStreamAdded(event);
  74. };
  75. this.peerconnection.onremovestream = function (event) {
  76. // Remove the stream from remoteStreams
  77. // FIXME: remotestreamremoved.jingle not defined anywhere(unused)
  78. $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
  79. };
  80. this.peerconnection.onsignalingstatechange = function (event) {
  81. if (!(self && self.peerconnection)) return;
  82. };
  83. this.peerconnection.oniceconnectionstatechange = function (event) {
  84. if (!(self && self.peerconnection)) return;
  85. switch (self.peerconnection.iceConnectionState) {
  86. case 'connected':
  87. this.startTime = new Date();
  88. break;
  89. case 'disconnected':
  90. this.stopTime = new Date();
  91. break;
  92. }
  93. onIceConnectionStateChange(self.sid, self);
  94. };
  95. // add any local and relayed stream
  96. RTC.localStreams.forEach(function(stream) {
  97. self.peerconnection.addStream(stream.getOriginalStream());
  98. });
  99. this.relayedStreams.forEach(function(stream) {
  100. self.peerconnection.addStream(stream);
  101. });
  102. };
  103. function onIceConnectionStateChange(sid, session) {
  104. switch (session.peerconnection.iceConnectionState) {
  105. case 'checking':
  106. session.timeChecking = (new Date()).getTime();
  107. session.firstconnect = true;
  108. break;
  109. case 'completed': // on caller side
  110. case 'connected':
  111. if (session.firstconnect) {
  112. session.firstconnect = false;
  113. var metadata = {};
  114. metadata.setupTime
  115. = (new Date()).getTime() - session.timeChecking;
  116. session.peerconnection.getStats(function (res) {
  117. if(res && res.result) {
  118. res.result().forEach(function (report) {
  119. if (report.type == 'googCandidatePair' &&
  120. report.stat('googActiveConnection') == 'true') {
  121. metadata.localCandidateType
  122. = report.stat('googLocalCandidateType');
  123. metadata.remoteCandidateType
  124. = report.stat('googRemoteCandidateType');
  125. // log pair as well so we can get nice pie
  126. // charts
  127. metadata.candidatePair
  128. = report.stat('googLocalCandidateType') +
  129. ';' +
  130. report.stat('googRemoteCandidateType');
  131. if (report.stat('googRemoteAddress').indexOf('[') === 0)
  132. {
  133. metadata.ipv6 = true;
  134. }
  135. }
  136. });
  137. }
  138. });
  139. }
  140. break;
  141. }
  142. }
  143. JingleSession.prototype.accept = function () {
  144. var self = this;
  145. this.state = 'active';
  146. var pranswer = this.peerconnection.localDescription;
  147. if (!pranswer || pranswer.type != 'pranswer') {
  148. return;
  149. }
  150. console.log('going from pranswer to answer');
  151. if (this.usetrickle) {
  152. // remove candidates already sent from session-accept
  153. var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
  154. for (var i = 0; i < lines.length; i++) {
  155. pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
  156. }
  157. }
  158. while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
  159. // FIXME: change any inactive to sendrecv or whatever they were originally
  160. pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
  161. }
  162. pranswer = simulcast.reverseTransformLocalDescription(pranswer);
  163. var prsdp = new SDP(pranswer.sdp);
  164. var accept = $iq({to: this.peerjid,
  165. type: 'set'})
  166. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  167. action: 'session-accept',
  168. initiator: this.initiator,
  169. responder: this.responder,
  170. sid: this.sid });
  171. prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
  172. var sdp = this.peerconnection.localDescription.sdp;
  173. while (SDPUtil.find_line(sdp, 'a=inactive')) {
  174. // FIXME: change any inactive to sendrecv or whatever they were originally
  175. sdp = sdp.replace('a=inactive', 'a=sendrecv');
  176. }
  177. var self = this;
  178. this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
  179. function () {
  180. //console.log('setLocalDescription success');
  181. self.setLocalDescription();
  182. self.connection.sendIQ(accept,
  183. function () {
  184. var ack = {};
  185. ack.source = 'answer';
  186. $(document).trigger('ack.jingle', [self.sid, ack]);
  187. },
  188. function (stanza) {
  189. var error = ($(stanza).find('error').length) ? {
  190. code: $(stanza).find('error').attr('code'),
  191. reason: $(stanza).find('error :first')[0].tagName
  192. }:{};
  193. error.source = 'answer';
  194. JingleSession.onJingleError(self.sid, error);
  195. },
  196. 10000);
  197. },
  198. function (e) {
  199. console.error('setLocalDescription failed', e);
  200. }
  201. );
  202. };
  203. JingleSession.prototype.terminate = function (reason) {
  204. this.state = 'ended';
  205. this.reason = reason;
  206. this.peerconnection.close();
  207. if (this.statsinterval !== null) {
  208. window.clearInterval(this.statsinterval);
  209. this.statsinterval = null;
  210. }
  211. };
  212. JingleSession.prototype.active = function () {
  213. return this.state == 'active';
  214. };
  215. JingleSession.prototype.sendIceCandidate = function (candidate) {
  216. var self = this;
  217. if (candidate && !this.lasticecandidate) {
  218. var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
  219. var jcand = SDPUtil.candidateToJingle(candidate.candidate);
  220. if (!(ice && jcand)) {
  221. console.error('failed to get ice && jcand');
  222. return;
  223. }
  224. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  225. if (jcand.type === 'srflx') {
  226. this.hadstuncandidate = true;
  227. } else if (jcand.type === 'relay') {
  228. this.hadturncandidate = true;
  229. }
  230. if (this.usetrickle) {
  231. if (this.usedrip) {
  232. if (this.drip_container.length === 0) {
  233. // start 20ms callout
  234. window.setTimeout(function () {
  235. if (self.drip_container.length === 0) return;
  236. self.sendIceCandidates(self.drip_container);
  237. self.drip_container = [];
  238. }, 20);
  239. }
  240. this.drip_container.push(candidate);
  241. return;
  242. } else {
  243. self.sendIceCandidate([candidate]);
  244. }
  245. }
  246. } else {
  247. //console.log('sendIceCandidate: last candidate.');
  248. if (!this.usetrickle) {
  249. //console.log('should send full offer now...');
  250. var init = $iq({to: this.peerjid,
  251. type: 'set'})
  252. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  253. action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
  254. initiator: this.initiator,
  255. sid: this.sid});
  256. this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
  257. var self = this;
  258. var sendJingle = function (ssrc) {
  259. if(!ssrc)
  260. ssrc = {};
  261. self.localSDP.toJingle(init, self.initiator == self.me ? 'initiator' : 'responder', ssrc);
  262. self.connection.sendIQ(init,
  263. function () {
  264. //console.log('session initiate ack');
  265. var ack = {};
  266. ack.source = 'offer';
  267. $(document).trigger('ack.jingle', [self.sid, ack]);
  268. },
  269. function (stanza) {
  270. self.state = 'error';
  271. self.peerconnection.close();
  272. var error = ($(stanza).find('error').length) ? {
  273. code: $(stanza).find('error').attr('code'),
  274. reason: $(stanza).find('error :first')[0].tagName,
  275. }:{};
  276. error.source = 'offer';
  277. JingleSession.onJingleError(self.sid, error);
  278. },
  279. 10000);
  280. }
  281. sendJingle();
  282. }
  283. this.lasticecandidate = true;
  284. console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
  285. console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
  286. if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
  287. $(document).trigger('nostuncandidates.jingle', [this.sid]);
  288. }
  289. }
  290. };
  291. JingleSession.prototype.sendIceCandidates = function (candidates) {
  292. console.log('sendIceCandidates', candidates);
  293. var cand = $iq({to: this.peerjid, type: 'set'})
  294. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  295. action: 'transport-info',
  296. initiator: this.initiator,
  297. sid: this.sid});
  298. for (var mid = 0; mid < this.localSDP.media.length; mid++) {
  299. var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
  300. var mline = SDPUtil.parse_mline(this.localSDP.media[mid].split('\r\n')[0]);
  301. if (cands.length > 0) {
  302. var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
  303. ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
  304. cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
  305. name: (cands[0].sdpMid? cands[0].sdpMid : mline.media)
  306. }).c('transport', ice);
  307. for (var i = 0; i < cands.length; i++) {
  308. cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
  309. }
  310. // add fingerprint
  311. if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
  312. var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
  313. tmp.required = true;
  314. cand.c(
  315. 'fingerprint',
  316. {xmlns: 'urn:xmpp:jingle:apps:dtls:0'})
  317. .t(tmp.fingerprint);
  318. delete tmp.fingerprint;
  319. cand.attrs(tmp);
  320. cand.up();
  321. }
  322. cand.up(); // transport
  323. cand.up(); // content
  324. }
  325. }
  326. // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
  327. //console.log('was this the last candidate', this.lasticecandidate);
  328. this.connection.sendIQ(cand,
  329. function () {
  330. var ack = {};
  331. ack.source = 'transportinfo';
  332. $(document).trigger('ack.jingle', [this.sid, ack]);
  333. },
  334. function (stanza) {
  335. var error = ($(stanza).find('error').length) ? {
  336. code: $(stanza).find('error').attr('code'),
  337. reason: $(stanza).find('error :first')[0].tagName,
  338. }:{};
  339. error.source = 'transportinfo';
  340. JingleSession.onJingleError(this.sid, error);
  341. },
  342. 10000);
  343. };
  344. JingleSession.prototype.sendOffer = function () {
  345. //console.log('sendOffer...');
  346. var self = this;
  347. this.peerconnection.createOffer(function (sdp) {
  348. self.createdOffer(sdp);
  349. },
  350. function (e) {
  351. console.error('createOffer failed', e);
  352. },
  353. this.media_constraints
  354. );
  355. };
  356. JingleSession.prototype.createdOffer = function (sdp) {
  357. //console.log('createdOffer', sdp);
  358. var self = this;
  359. this.localSDP = new SDP(sdp.sdp);
  360. //this.localSDP.mangle();
  361. var sendJingle = function () {
  362. var init = $iq({to: this.peerjid,
  363. type: 'set'})
  364. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  365. action: 'session-initiate',
  366. initiator: this.initiator,
  367. sid: this.sid});
  368. self.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
  369. self.connection.sendIQ(init,
  370. function () {
  371. var ack = {};
  372. ack.source = 'offer';
  373. $(document).trigger('ack.jingle', [self.sid, ack]);
  374. },
  375. function (stanza) {
  376. self.state = 'error';
  377. self.peerconnection.close();
  378. var error = ($(stanza).find('error').length) ? {
  379. code: $(stanza).find('error').attr('code'),
  380. reason: $(stanza).find('error :first')[0].tagName,
  381. }:{};
  382. error.source = 'offer';
  383. JingleSession.onJingleError(self.sid, error);
  384. },
  385. 10000);
  386. }
  387. sdp.sdp = this.localSDP.raw;
  388. this.peerconnection.setLocalDescription(sdp,
  389. function () {
  390. if(self.usetrickle)
  391. {
  392. sendJingle();
  393. }
  394. self.setLocalDescription();
  395. //console.log('setLocalDescription success');
  396. },
  397. function (e) {
  398. console.error('setLocalDescription failed', e);
  399. }
  400. );
  401. var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
  402. for (var i = 0; i < cands.length; i++) {
  403. var cand = SDPUtil.parse_icecandidate(cands[i]);
  404. if (cand.type == 'srflx') {
  405. this.hadstuncandidate = true;
  406. } else if (cand.type == 'relay') {
  407. this.hadturncandidate = true;
  408. }
  409. }
  410. };
  411. JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
  412. //console.log('setting remote description... ', desctype);
  413. this.remoteSDP = new SDP('');
  414. this.remoteSDP.fromJingle(elem);
  415. if (this.peerconnection.remoteDescription !== null) {
  416. console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
  417. if (this.peerconnection.remoteDescription.type == 'pranswer') {
  418. var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
  419. for (var i = 0; i < pranswer.media.length; i++) {
  420. // make sure we have ice ufrag and pwd
  421. if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
  422. if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
  423. this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
  424. } else {
  425. console.warn('no ice ufrag?');
  426. }
  427. if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
  428. this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
  429. } else {
  430. console.warn('no ice pwd?');
  431. }
  432. }
  433. // copy over candidates
  434. var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
  435. for (var j = 0; j < lines.length; j++) {
  436. this.remoteSDP.media[i] += lines[j] + '\r\n';
  437. }
  438. }
  439. this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
  440. }
  441. }
  442. var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
  443. this.peerconnection.setRemoteDescription(remotedesc,
  444. function () {
  445. //console.log('setRemoteDescription success');
  446. },
  447. function (e) {
  448. console.error('setRemoteDescription error', e);
  449. JingleSession.onJingleFatalError(self, e);
  450. }
  451. );
  452. };
  453. JingleSession.prototype.addIceCandidate = function (elem) {
  454. var self = this;
  455. if (this.peerconnection.signalingState == 'closed') {
  456. return;
  457. }
  458. if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
  459. console.log('trickle ice candidate arriving before session accept...');
  460. // create a PRANSWER for setRemoteDescription
  461. if (!this.remoteSDP) {
  462. var cobbled = 'v=0\r\n' +
  463. 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
  464. 's=-\r\n' +
  465. 't=0 0\r\n';
  466. // first, take some things from the local description
  467. for (var i = 0; i < this.localSDP.media.length; i++) {
  468. cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
  469. cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
  470. if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
  471. cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
  472. }
  473. cobbled += 'a=inactive\r\n';
  474. }
  475. this.remoteSDP = new SDP(cobbled);
  476. }
  477. // then add things like ice and dtls from remote candidate
  478. elem.each(function () {
  479. for (var i = 0; i < self.remoteSDP.media.length; i++) {
  480. if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  481. self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  482. if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
  483. var tmp = $(this).find('transport');
  484. self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  485. self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  486. tmp = $(this).find('transport>fingerprint');
  487. if (tmp.length) {
  488. self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  489. } else {
  490. console.log('no dtls fingerprint (webrtc issue #1718?)');
  491. self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
  492. }
  493. break;
  494. }
  495. }
  496. }
  497. });
  498. this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
  499. // we need a complete SDP with ice-ufrag/ice-pwd in all parts
  500. // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
  501. // but it could be in the session part as well. since the code above constructs this sdp this can't happen however
  502. var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
  503. return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
  504. }).length == this.remoteSDP.media.length;
  505. if (iscomplete) {
  506. console.log('setting pranswer');
  507. try {
  508. this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
  509. function() {
  510. },
  511. function(e) {
  512. console.log('setRemoteDescription pranswer failed', e.toString());
  513. });
  514. } catch (e) {
  515. console.error('setting pranswer failed', e);
  516. }
  517. } else {
  518. //console.log('not yet setting pranswer');
  519. }
  520. }
  521. // operate on each content element
  522. elem.each(function () {
  523. // would love to deactivate this, but firefox still requires it
  524. var idx = -1;
  525. var i;
  526. for (i = 0; i < self.remoteSDP.media.length; i++) {
  527. if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  528. self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  529. idx = i;
  530. break;
  531. }
  532. }
  533. if (idx == -1) { // fall back to localdescription
  534. for (i = 0; i < self.localSDP.media.length; i++) {
  535. if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
  536. self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
  537. idx = i;
  538. break;
  539. }
  540. }
  541. }
  542. var name = $(this).attr('name');
  543. // TODO: check ice-pwd and ice-ufrag?
  544. $(this).find('transport>candidate').each(function () {
  545. var line, candidate;
  546. line = SDPUtil.candidateFromJingle(this);
  547. candidate = new RTCIceCandidate({sdpMLineIndex: idx,
  548. sdpMid: name,
  549. candidate: line});
  550. try {
  551. self.peerconnection.addIceCandidate(candidate);
  552. } catch (e) {
  553. console.error('addIceCandidate failed', e.toString(), line);
  554. }
  555. });
  556. });
  557. };
  558. JingleSession.prototype.sendAnswer = function (provisional) {
  559. //console.log('createAnswer', provisional);
  560. var self = this;
  561. this.peerconnection.createAnswer(
  562. function (sdp) {
  563. self.createdAnswer(sdp, provisional);
  564. },
  565. function (e) {
  566. console.error('createAnswer failed', e);
  567. },
  568. this.media_constraints
  569. );
  570. };
  571. JingleSession.prototype.createdAnswer = function (sdp, provisional) {
  572. //console.log('createAnswer callback');
  573. var self = this;
  574. this.localSDP = new SDP(sdp.sdp);
  575. //this.localSDP.mangle();
  576. this.usepranswer = provisional === true;
  577. if (this.usetrickle) {
  578. if (this.usepranswer) {
  579. sdp.type = 'pranswer';
  580. for (var i = 0; i < this.localSDP.media.length; i++) {
  581. this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
  582. }
  583. this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
  584. }
  585. }
  586. var self = this;
  587. var sendJingle = function (ssrcs) {
  588. var accept = $iq({to: self.peerjid,
  589. type: 'set'})
  590. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  591. action: 'session-accept',
  592. initiator: self.initiator,
  593. responder: self.responder,
  594. sid: self.sid });
  595. var publicLocalDesc = simulcast.reverseTransformLocalDescription(sdp);
  596. var publicLocalSDP = new SDP(publicLocalDesc.sdp);
  597. publicLocalSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs);
  598. self.connection.sendIQ(accept,
  599. function () {
  600. var ack = {};
  601. ack.source = 'answer';
  602. $(document).trigger('ack.jingle', [self.sid, ack]);
  603. },
  604. function (stanza) {
  605. var error = ($(stanza).find('error').length) ? {
  606. code: $(stanza).find('error').attr('code'),
  607. reason: $(stanza).find('error :first')[0].tagName,
  608. }:{};
  609. error.source = 'answer';
  610. JingleSession.onJingleError(self.sid, error);
  611. },
  612. 10000);
  613. }
  614. sdp.sdp = this.localSDP.raw;
  615. this.peerconnection.setLocalDescription(sdp,
  616. function () {
  617. //console.log('setLocalDescription success');
  618. if (self.usetrickle && !self.usepranswer) {
  619. sendJingle();
  620. }
  621. self.setLocalDescription();
  622. },
  623. function (e) {
  624. console.error('setLocalDescription failed', e);
  625. }
  626. );
  627. var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
  628. for (var j = 0; j < cands.length; j++) {
  629. var cand = SDPUtil.parse_icecandidate(cands[j]);
  630. if (cand.type == 'srflx') {
  631. this.hadstuncandidate = true;
  632. } else if (cand.type == 'relay') {
  633. this.hadturncandidate = true;
  634. }
  635. }
  636. };
  637. JingleSession.prototype.sendTerminate = function (reason, text) {
  638. var self = this,
  639. term = $iq({to: this.peerjid,
  640. type: 'set'})
  641. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  642. action: 'session-terminate',
  643. initiator: this.initiator,
  644. sid: this.sid})
  645. .c('reason')
  646. .c(reason || 'success');
  647. if (text) {
  648. term.up().c('text').t(text);
  649. }
  650. this.connection.sendIQ(term,
  651. function () {
  652. self.peerconnection.close();
  653. self.peerconnection = null;
  654. self.terminate();
  655. var ack = {};
  656. ack.source = 'terminate';
  657. $(document).trigger('ack.jingle', [self.sid, ack]);
  658. },
  659. function (stanza) {
  660. var error = ($(stanza).find('error').length) ? {
  661. code: $(stanza).find('error').attr('code'),
  662. reason: $(stanza).find('error :first')[0].tagName,
  663. }:{};
  664. $(document).trigger('ack.jingle', [self.sid, error]);
  665. },
  666. 10000);
  667. if (this.statsinterval !== null) {
  668. window.clearInterval(this.statsinterval);
  669. this.statsinterval = null;
  670. }
  671. };
  672. JingleSession.prototype.addSource = function (elem, fromJid) {
  673. var self = this;
  674. // FIXME: dirty waiting
  675. if (!this.peerconnection.localDescription)
  676. {
  677. console.warn("addSource - localDescription not ready yet")
  678. setTimeout(function()
  679. {
  680. self.addSource(elem, fromJid);
  681. },
  682. 200
  683. );
  684. return;
  685. }
  686. console.log('addssrc', new Date().getTime());
  687. console.log('ice', this.peerconnection.iceConnectionState);
  688. var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
  689. var mySdp = new SDP(this.peerconnection.localDescription.sdp);
  690. $(elem).each(function (idx, content) {
  691. var name = $(content).attr('name');
  692. var lines = '';
  693. tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
  694. var semantics = this.getAttribute('semantics');
  695. var ssrcs = $(this).find('>source').map(function () {
  696. return this.getAttribute('ssrc');
  697. }).get();
  698. if (ssrcs.length != 0) {
  699. lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
  700. }
  701. });
  702. tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
  703. tmp.each(function () {
  704. var ssrc = $(this).attr('ssrc');
  705. if(mySdp.containsSSRC(ssrc)){
  706. /**
  707. * This happens when multiple participants change their streams at the same time and
  708. * ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
  709. * addssrc are scheduled for update IQ. See
  710. */
  711. console.warn("Got add stream request for my own ssrc: "+ssrc);
  712. return;
  713. }
  714. $(this).find('>parameter').each(function () {
  715. lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
  716. if ($(this).attr('value') && $(this).attr('value').length)
  717. lines += ':' + $(this).attr('value');
  718. lines += '\r\n';
  719. });
  720. });
  721. sdp.media.forEach(function(media, idx) {
  722. if (!SDPUtil.find_line(media, 'a=mid:' + name))
  723. return;
  724. sdp.media[idx] += lines;
  725. if (!self.addssrc[idx]) self.addssrc[idx] = '';
  726. self.addssrc[idx] += lines;
  727. });
  728. sdp.raw = sdp.session + sdp.media.join('');
  729. });
  730. this.modifySources();
  731. };
  732. JingleSession.prototype.removeSource = function (elem, fromJid) {
  733. var self = this;
  734. // FIXME: dirty waiting
  735. if (!this.peerconnection.localDescription)
  736. {
  737. console.warn("removeSource - localDescription not ready yet")
  738. setTimeout(function()
  739. {
  740. self.removeSource(elem, fromJid);
  741. },
  742. 200
  743. );
  744. return;
  745. }
  746. console.log('removessrc', new Date().getTime());
  747. console.log('ice', this.peerconnection.iceConnectionState);
  748. var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
  749. var mySdp = new SDP(this.peerconnection.localDescription.sdp);
  750. $(elem).each(function (idx, content) {
  751. var name = $(content).attr('name');
  752. var lines = '';
  753. tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
  754. var semantics = this.getAttribute('semantics');
  755. var ssrcs = $(this).find('>source').map(function () {
  756. return this.getAttribute('ssrc');
  757. }).get();
  758. if (ssrcs.length != 0) {
  759. lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
  760. }
  761. });
  762. tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
  763. tmp.each(function () {
  764. var ssrc = $(this).attr('ssrc');
  765. // This should never happen, but can be useful for bug detection
  766. if(mySdp.containsSSRC(ssrc)){
  767. console.error("Got remove stream request for my own ssrc: "+ssrc);
  768. return;
  769. }
  770. $(this).find('>parameter').each(function () {
  771. lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
  772. if ($(this).attr('value') && $(this).attr('value').length)
  773. lines += ':' + $(this).attr('value');
  774. lines += '\r\n';
  775. });
  776. });
  777. sdp.media.forEach(function(media, idx) {
  778. if (!SDPUtil.find_line(media, 'a=mid:' + name))
  779. return;
  780. sdp.media[idx] += lines;
  781. if (!self.removessrc[idx]) self.removessrc[idx] = '';
  782. self.removessrc[idx] += lines;
  783. });
  784. sdp.raw = sdp.session + sdp.media.join('');
  785. });
  786. this.modifySources();
  787. };
  788. JingleSession.prototype.modifySources = function (successCallback) {
  789. var self = this;
  790. if (this.peerconnection.signalingState == 'closed') return;
  791. if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
  792. // There is nothing to do since scheduled job might have been executed by another succeeding call
  793. this.setLocalDescription();
  794. if(successCallback){
  795. successCallback();
  796. }
  797. return;
  798. }
  799. // FIXME: this is a big hack
  800. // https://code.google.com/p/webrtc/issues/detail?id=2688
  801. // ^ has been fixed.
  802. if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
  803. console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
  804. this.wait = true;
  805. window.setTimeout(function() { self.modifySources(successCallback); }, 250);
  806. return;
  807. }
  808. if (this.wait) {
  809. window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
  810. this.wait = false;
  811. return;
  812. }
  813. // Reset switch streams flag
  814. this.switchstreams = false;
  815. var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
  816. // add sources
  817. this.addssrc.forEach(function(lines, idx) {
  818. sdp.media[idx] += lines;
  819. });
  820. this.addssrc = [];
  821. // remove sources
  822. this.removessrc.forEach(function(lines, idx) {
  823. lines = lines.split('\r\n');
  824. lines.pop(); // remove empty last element;
  825. lines.forEach(function(line) {
  826. sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
  827. });
  828. });
  829. this.removessrc = [];
  830. // FIXME:
  831. // this was a hack for the situation when only one peer exists
  832. // in the conference.
  833. // check if still required and remove
  834. if (sdp.media[0])
  835. sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv');
  836. if (sdp.media[1])
  837. sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
  838. sdp.raw = sdp.session + sdp.media.join('');
  839. this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
  840. function() {
  841. if(self.signalingState == 'closed') {
  842. console.error("createAnswer attempt on closed state");
  843. return;
  844. }
  845. self.peerconnection.createAnswer(
  846. function(modifiedAnswer) {
  847. // change video direction, see https://github.com/jitsi/jitmeet/issues/41
  848. if (self.pendingop !== null) {
  849. var sdp = new SDP(modifiedAnswer.sdp);
  850. if (sdp.media.length > 1) {
  851. switch(self.pendingop) {
  852. case 'mute':
  853. sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
  854. break;
  855. case 'unmute':
  856. sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
  857. break;
  858. }
  859. sdp.raw = sdp.session + sdp.media.join('');
  860. modifiedAnswer.sdp = sdp.raw;
  861. }
  862. self.pendingop = null;
  863. }
  864. // FIXME: pushing down an answer while ice connection state
  865. // is still checking is bad...
  866. //console.log(self.peerconnection.iceConnectionState);
  867. // trying to work around another chrome bug
  868. //modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
  869. self.peerconnection.setLocalDescription(modifiedAnswer,
  870. function() {
  871. //console.log('modified setLocalDescription ok');
  872. self.setLocalDescription();
  873. if(successCallback){
  874. successCallback();
  875. }
  876. },
  877. function(error) {
  878. console.error('modified setLocalDescription failed', error);
  879. }
  880. );
  881. },
  882. function(error) {
  883. console.error('modified answer failed', error);
  884. }
  885. );
  886. },
  887. function(error) {
  888. console.error('modify failed', error);
  889. }
  890. );
  891. };
  892. /**
  893. * Switches video streams.
  894. * @param new_stream new stream that will be used as video of this session.
  895. * @param oldStream old video stream of this session.
  896. * @param success_callback callback executed after successful stream switch.
  897. */
  898. JingleSession.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
  899. var self = this;
  900. // Remember SDP to figure out added/removed SSRCs
  901. var oldSdp = null;
  902. if(self.peerconnection) {
  903. if(self.peerconnection.localDescription) {
  904. oldSdp = new SDP(self.peerconnection.localDescription.sdp);
  905. }
  906. self.peerconnection.removeStream(oldStream, true);
  907. self.peerconnection.addStream(new_stream);
  908. }
  909. RTC.switchVideoStreams(new_stream, oldStream);
  910. // Conference is not active
  911. if(!oldSdp || !self.peerconnection) {
  912. success_callback();
  913. return;
  914. }
  915. self.switchstreams = true;
  916. self.modifySources(function() {
  917. console.log('modify sources done');
  918. success_callback();
  919. var newSdp = new SDP(self.peerconnection.localDescription.sdp);
  920. console.log("SDPs", oldSdp, newSdp);
  921. self.notifyMySSRCUpdate(oldSdp, newSdp);
  922. });
  923. };
  924. /**
  925. * Figures out added/removed ssrcs and send update IQs.
  926. * @param old_sdp SDP object for old description.
  927. * @param new_sdp SDP object for new description.
  928. */
  929. JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
  930. if (!(this.peerconnection.signalingState == 'stable' &&
  931. this.peerconnection.iceConnectionState == 'connected')){
  932. console.log("Too early to send updates");
  933. return;
  934. }
  935. // send source-remove IQ.
  936. sdpDiffer = new SDPDiffer(new_sdp, old_sdp);
  937. var remove = $iq({to: this.peerjid, type: 'set'})
  938. .c('jingle', {
  939. xmlns: 'urn:xmpp:jingle:1',
  940. action: 'source-remove',
  941. initiator: this.initiator,
  942. sid: this.sid
  943. }
  944. );
  945. var removed = sdpDiffer.toJingle(remove);
  946. if (removed) {
  947. this.connection.sendIQ(remove,
  948. function (res) {
  949. console.info('got remove result', res);
  950. },
  951. function (err) {
  952. console.error('got remove error', err);
  953. }
  954. );
  955. } else {
  956. console.log('removal not necessary');
  957. }
  958. // send source-add IQ.
  959. var sdpDiffer = new SDPDiffer(old_sdp, new_sdp);
  960. var add = $iq({to: this.peerjid, type: 'set'})
  961. .c('jingle', {
  962. xmlns: 'urn:xmpp:jingle:1',
  963. action: 'source-add',
  964. initiator: this.initiator,
  965. sid: this.sid
  966. }
  967. );
  968. var added = sdpDiffer.toJingle(add);
  969. if (added) {
  970. this.connection.sendIQ(add,
  971. function (res) {
  972. console.info('got add result', res);
  973. },
  974. function (err) {
  975. console.error('got add error', err);
  976. }
  977. );
  978. } else {
  979. console.log('addition not necessary');
  980. }
  981. };
  982. /**
  983. * Determines whether the (local) video is mute i.e. all video tracks are
  984. * disabled.
  985. *
  986. * @return <tt>true</tt> if the (local) video is mute i.e. all video tracks are
  987. * disabled; otherwise, <tt>false</tt>
  988. */
  989. JingleSession.prototype.isVideoMute = function () {
  990. var tracks = RTC.localVideo.getVideoTracks();
  991. var mute = true;
  992. for (var i = 0; i < tracks.length; ++i) {
  993. if (tracks[i].enabled) {
  994. mute = false;
  995. break;
  996. }
  997. }
  998. return mute;
  999. };
  1000. /**
  1001. * Mutes/unmutes the (local) video i.e. enables/disables all video tracks.
  1002. *
  1003. * @param mute <tt>true</tt> to mute the (local) video i.e. to disable all video
  1004. * tracks; otherwise, <tt>false</tt>
  1005. * @param callback a function to be invoked with <tt>mute</tt> after all video
  1006. * tracks have been enabled/disabled. The function may, optionally, return
  1007. * another function which is to be invoked after the whole mute/unmute operation
  1008. * has completed successfully.
  1009. * @param options an object which specifies optional arguments such as the
  1010. * <tt>boolean</tt> key <tt>byUser</tt> with default value <tt>true</tt> which
  1011. * specifies whether the method was initiated in response to a user command (in
  1012. * contrast to an automatic decision made by the application logic)
  1013. */
  1014. JingleSession.prototype.setVideoMute = function (mute, callback, options) {
  1015. var byUser;
  1016. if (options) {
  1017. byUser = options.byUser;
  1018. if (typeof byUser === 'undefined') {
  1019. byUser = true;
  1020. }
  1021. } else {
  1022. byUser = true;
  1023. }
  1024. // The user's command to mute the (local) video takes precedence over any
  1025. // automatic decision made by the application logic.
  1026. if (byUser) {
  1027. this.videoMuteByUser = mute;
  1028. } else if (this.videoMuteByUser) {
  1029. return;
  1030. }
  1031. var self = this;
  1032. var localCallback = function (mute) {
  1033. self.connection.emuc.addVideoInfoToPresence(mute);
  1034. self.connection.emuc.sendPresence();
  1035. return callback(mute)
  1036. };
  1037. if (mute == RTC.localVideo.isMuted())
  1038. {
  1039. // Even if no change occurs, the specified callback is to be executed.
  1040. // The specified callback may, optionally, return a successCallback
  1041. // which is to be executed as well.
  1042. var successCallback = localCallback(mute);
  1043. if (successCallback) {
  1044. successCallback();
  1045. }
  1046. } else {
  1047. RTC.localVideo.setMute(!mute);
  1048. this.hardMuteVideo(mute);
  1049. this.modifySources(localCallback(mute));
  1050. }
  1051. };
  1052. // SDP-based mute by going recvonly/sendrecv
  1053. // FIXME: should probably black out the screen as well
  1054. JingleSession.prototype.toggleVideoMute = function (callback) {
  1055. this.service.setVideoMute(RTC.localVideo.isMuted(), callback);
  1056. };
  1057. JingleSession.prototype.hardMuteVideo = function (muted) {
  1058. this.pendingop = muted ? 'mute' : 'unmute';
  1059. };
  1060. JingleSession.prototype.sendMute = function (muted, content) {
  1061. var info = $iq({to: this.peerjid,
  1062. type: 'set'})
  1063. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  1064. action: 'session-info',
  1065. initiator: this.initiator,
  1066. sid: this.sid });
  1067. info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
  1068. info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
  1069. if (content) {
  1070. info.attrs({'name': content});
  1071. }
  1072. this.connection.send(info);
  1073. };
  1074. JingleSession.prototype.sendRinging = function () {
  1075. var info = $iq({to: this.peerjid,
  1076. type: 'set'})
  1077. .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
  1078. action: 'session-info',
  1079. initiator: this.initiator,
  1080. sid: this.sid });
  1081. info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
  1082. this.connection.send(info);
  1083. };
  1084. JingleSession.prototype.getStats = function (interval) {
  1085. var self = this;
  1086. var recv = {audio: 0, video: 0};
  1087. var lost = {audio: 0, video: 0};
  1088. var lastrecv = {audio: 0, video: 0};
  1089. var lastlost = {audio: 0, video: 0};
  1090. var loss = {audio: 0, video: 0};
  1091. var delta = {audio: 0, video: 0};
  1092. this.statsinterval = window.setInterval(function () {
  1093. if (self && self.peerconnection && self.peerconnection.getStats) {
  1094. self.peerconnection.getStats(function (stats) {
  1095. var results = stats.result();
  1096. // TODO: there are so much statistics you can get from this..
  1097. for (var i = 0; i < results.length; ++i) {
  1098. if (results[i].type == 'ssrc') {
  1099. var packetsrecv = results[i].stat('packetsReceived');
  1100. var packetslost = results[i].stat('packetsLost');
  1101. if (packetsrecv && packetslost) {
  1102. packetsrecv = parseInt(packetsrecv, 10);
  1103. packetslost = parseInt(packetslost, 10);
  1104. if (results[i].stat('googFrameRateReceived')) {
  1105. lastlost.video = lost.video;
  1106. lastrecv.video = recv.video;
  1107. recv.video = packetsrecv;
  1108. lost.video = packetslost;
  1109. } else {
  1110. lastlost.audio = lost.audio;
  1111. lastrecv.audio = recv.audio;
  1112. recv.audio = packetsrecv;
  1113. lost.audio = packetslost;
  1114. }
  1115. }
  1116. }
  1117. }
  1118. delta.audio = recv.audio - lastrecv.audio;
  1119. delta.video = recv.video - lastrecv.video;
  1120. loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
  1121. loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
  1122. $(document).trigger('packetloss.jingle', [self.sid, loss]);
  1123. });
  1124. }
  1125. }, interval || 3000);
  1126. return this.statsinterval;
  1127. };
  1128. JingleSession.onJingleError = function (session, error)
  1129. {
  1130. console.error("Jingle error", error);
  1131. }
  1132. JingleSession.onJingleFatalError = function (session, error)
  1133. {
  1134. this.service.sessionTerminated = true;
  1135. connection.emuc.doLeave();
  1136. UI.messageHandler.showError( "Sorry",
  1137. "Internal application error[setRemoteDescription]");
  1138. }
  1139. JingleSession.prototype.setLocalDescription = function () {
  1140. // put our ssrcs into presence so other clients can identify our stream
  1141. var newssrcs = [];
  1142. var media = simulcast.parseMedia(this.peerconnection.localDescription);
  1143. media.forEach(function (media) {
  1144. if(Object.keys(media.sources).length > 0) {
  1145. // TODO(gp) maybe exclude FID streams?
  1146. Object.keys(media.sources).forEach(function (ssrc) {
  1147. newssrcs.push({
  1148. 'ssrc': ssrc,
  1149. 'type': media.type,
  1150. 'direction': media.direction
  1151. });
  1152. });
  1153. }
  1154. else if(this.localStreamsSSRC && this.localStreamsSSRC[media.type])
  1155. {
  1156. newssrcs.push({
  1157. 'ssrc': this.localStreamsSSRC[media.type],
  1158. 'type': media.type,
  1159. 'direction': media.direction
  1160. });
  1161. }
  1162. });
  1163. console.log('new ssrcs', newssrcs);
  1164. // Have to clear presence map to get rid of removed streams
  1165. this.connection.emuc.clearPresenceMedia();
  1166. if (newssrcs.length > 0) {
  1167. for (var i = 1; i <= newssrcs.length; i ++) {
  1168. // Change video type to screen
  1169. if (newssrcs[i-1].type === 'video' && desktopsharing.isUsingScreenStream()) {
  1170. newssrcs[i-1].type = 'screen';
  1171. }
  1172. this.connection.emuc.addMediaToPresence(i,
  1173. newssrcs[i-1].type, newssrcs[i-1].ssrc, newssrcs[i-1].direction);
  1174. }
  1175. this.connection.emuc.sendPresence();
  1176. }
  1177. }
  1178. // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
  1179. function sendKeyframe(pc) {
  1180. console.log('sendkeyframe', pc.iceConnectionState);
  1181. if (pc.iceConnectionState !== 'connected') return; // safe...
  1182. pc.setRemoteDescription(
  1183. pc.remoteDescription,
  1184. function () {
  1185. pc.createAnswer(
  1186. function (modifiedAnswer) {
  1187. pc.setLocalDescription(
  1188. modifiedAnswer,
  1189. function () {
  1190. // noop
  1191. },
  1192. function (error) {
  1193. console.log('triggerKeyframe setLocalDescription failed', error);
  1194. UI.messageHandler.showError();
  1195. }
  1196. );
  1197. },
  1198. function (error) {
  1199. console.log('triggerKeyframe createAnswer failed', error);
  1200. UI.messageHandler.showError();
  1201. }
  1202. );
  1203. },
  1204. function (error) {
  1205. console.log('triggerKeyframe setRemoteDescription failed', error);
  1206. UI.messageHandler.showError();
  1207. }
  1208. );
  1209. }
  1210. JingleSession.prototype.remoteStreamAdded = function (data) {
  1211. var self = this;
  1212. var thessrc;
  1213. var ssrc2jid = this.connection.emuc.ssrc2jid;
  1214. // look up an associated JID for a stream id
  1215. if (data.stream.id && data.stream.id.indexOf('mixedmslabel') === -1) {
  1216. // look only at a=ssrc: and _not_ at a=ssrc-group: lines
  1217. var ssrclines
  1218. = SDPUtil.find_lines(this.peerconnection.remoteDescription.sdp, 'a=ssrc:');
  1219. ssrclines = ssrclines.filter(function (line) {
  1220. // NOTE(gp) previously we filtered on the mslabel, but that property
  1221. // is not always present.
  1222. // return line.indexOf('mslabel:' + data.stream.label) !== -1;
  1223. return ((line.indexOf('msid:' + data.stream.id) !== -1));
  1224. });
  1225. if (ssrclines.length) {
  1226. thessrc = ssrclines[0].substring(7).split(' ')[0];
  1227. // We signal our streams (through Jingle to the focus) before we set
  1228. // our presence (through which peers associate remote streams to
  1229. // jids). So, it might arrive that a remote stream is added but
  1230. // ssrc2jid is not yet updated and thus data.peerjid cannot be
  1231. // successfully set. Here we wait for up to a second for the
  1232. // presence to arrive.
  1233. if (!ssrc2jid[thessrc]) {
  1234. // TODO(gp) limit wait duration to 1 sec.
  1235. setTimeout(function(d) {
  1236. return function() {
  1237. self.remoteStreamAdded(d);
  1238. }
  1239. }(data), 250);
  1240. return;
  1241. }
  1242. // ok to overwrite the one from focus? might save work in colibri.js
  1243. console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
  1244. if (ssrc2jid[thessrc]) {
  1245. data.peerjid = ssrc2jid[thessrc];
  1246. }
  1247. }
  1248. }
  1249. //TODO: this code should be removed when firefox implement multistream support
  1250. if(RTC.getBrowserType() == RTCBrowserType.RTC_BROWSER_FIREFOX)
  1251. {
  1252. if((JingleSession.notReceivedSSRCs.length == 0) ||
  1253. !ssrc2jid[JingleSession.notReceivedSSRCs[JingleSession.notReceivedSSRCs.length - 1]])
  1254. {
  1255. // TODO(gp) limit wait duration to 1 sec.
  1256. setTimeout(function(d) {
  1257. return function() {
  1258. self.remoteStreamAdded(d);
  1259. }
  1260. }(data), 250);
  1261. return;
  1262. }
  1263. thessrc = JingleSession.notReceivedSSRCs.pop();
  1264. if (ssrc2jid[thessrc]) {
  1265. data.peerjid = ssrc2jid[thessrc];
  1266. }
  1267. }
  1268. RTC.createRemoteStream(data, this.sid, thessrc);
  1269. var isVideo = data.stream.getVideoTracks().length > 0;
  1270. // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
  1271. if (isVideo &&
  1272. data.peerjid && this.peerjid === data.peerjid &&
  1273. data.stream.getVideoTracks().length === 0 &&
  1274. RTC.localVideo.getTracks().length > 0) {
  1275. window.setTimeout(function () {
  1276. sendKeyframe(self.peerconnection);
  1277. }, 3000);
  1278. }
  1279. }
  1280. module.exports = JingleSession;