您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

JingleSessionPC.js 61KB

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