Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

JingleSessionPC.js 56KB

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