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

strophe.jingle.session.js 45KB

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