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

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