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

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