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

colibri.focus.js 60KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643
  1. /* colibri.js -- a COLIBRI focus
  2. * The colibri spec has been submitted to the XMPP Standards Foundation
  3. * for publications as a XMPP extensions:
  4. * http://xmpp.org/extensions/inbox/colibri.html
  5. *
  6. * colibri.js is a participating focus, i.e. the focus participates
  7. * in the conference. The conference itself can be ad-hoc, through a
  8. * MUC, through PubSub, etc.
  9. *
  10. * colibri.js relies heavily on the strophe.jingle library available
  11. * from https://github.com/ESTOS/strophe.jingle
  12. * and interoperates with the Jitsi videobridge available from
  13. * https://jitsi.org/Projects/JitsiVideobridge
  14. */
  15. /*
  16. Copyright (c) 2013 ESTOS GmbH
  17. Permission is hereby granted, free of charge, to any person obtaining a copy
  18. of this software and associated documentation files (the "Software"), to deal
  19. in the Software without restriction, including without limitation the rights
  20. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  21. copies of the Software, and to permit persons to whom the Software is
  22. furnished to do so, subject to the following conditions:
  23. The above copyright notice and this permission notice shall be included in
  24. all copies or substantial portions of the Software.
  25. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  26. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  27. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  28. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  29. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  30. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  31. THE SOFTWARE.
  32. */
  33. /* jshint -W117 */
  34. ColibriFocus.prototype = Object.create(SessionBase.prototype);
  35. function ColibriFocus(connection, bridgejid) {
  36. SessionBase.call(this, connection, Math.random().toString(36).substr(2, 12));
  37. this.bridgejid = bridgejid;
  38. this.peers = [];
  39. this.remoteStreams = [];
  40. this.confid = null;
  41. /**
  42. * Local XMPP resource used to join the multi user chat.
  43. * @type {*}
  44. */
  45. this.myMucResource = Strophe.getResourceFromJid(connection.emuc.myroomjid);
  46. /**
  47. * Default channel expire value in seconds.
  48. * @type {number}
  49. */
  50. this.channelExpire
  51. = ('number' === typeof(config.channelExpire))
  52. ? config.channelExpire
  53. : 15;
  54. /**
  55. * Default channel last-n value.
  56. * @type {number}
  57. */
  58. this.channelLastN
  59. = ('number' === typeof(config.channelLastN)) ? config.channelLastN : -1;
  60. // media types of the conference
  61. if (config.openSctp)
  62. this.media = ['audio', 'video', 'data'];
  63. else
  64. this.media = ['audio', 'video'];
  65. this.connection.jingle.sessions[this.sid] = this;
  66. this.bundledTransports = {};
  67. this.mychannel = [];
  68. this.channels = [];
  69. this.remotessrc = {};
  70. // container for candidates from the focus
  71. // gathered before confid is known
  72. this.drip_container = [];
  73. // silly wait flag
  74. this.wait = true;
  75. this.recordingEnabled = false;
  76. // stores information about the endpoints (i.e. display names) to
  77. // be sent to the videobridge.
  78. this.endpointsInfo = null;
  79. }
  80. function conferenceCreated(focus)
  81. {
  82. statistics.onConfereceCreated(getConferenceHandler());
  83. RTC.onConfereceCreated(focus);
  84. }
  85. // creates a conferences with an initial set of peers
  86. ColibriFocus.prototype.makeConference = function (peers, errorCallback) {
  87. var self = this;
  88. if (this.confid !== null) {
  89. console.error('makeConference called twice? Ignoring...');
  90. // FIXME: just invite peers?
  91. return;
  92. }
  93. this.confid = 0; // !null
  94. this.peers = [];
  95. peers.forEach(function (peer) {
  96. self.peers.push(peer);
  97. self.channels.push([]);
  98. });
  99. this.peerconnection
  100. = new TraceablePeerConnection(
  101. this.connection.jingle.ice_config,
  102. this.connection.jingle.pc_constraints );
  103. if(this.connection.jingle.localAudio) {
  104. this.peerconnection.addStream(this.connection.jingle.localAudio);
  105. }
  106. if(this.connection.jingle.localVideo) {
  107. this.peerconnection.addStream(this.connection.jingle.localVideo);
  108. }
  109. this.peerconnection.oniceconnectionstatechange = function (event) {
  110. console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
  111. /*
  112. if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
  113. console.log('adding new remote SSRCs from iceconnectionstatechange');
  114. window.setTimeout(function() { self.modifySources(); }, 1000);
  115. }
  116. */
  117. $(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
  118. };
  119. this.peerconnection.onsignalingstatechange = function (event) {
  120. console.warn(self.peerconnection.signalingState);
  121. /*
  122. if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
  123. console.log('adding new remote SSRCs from signalingstatechange');
  124. window.setTimeout(function() { self.modifySources(); }, 1000);
  125. }
  126. */
  127. };
  128. this.peerconnection.onaddstream = function (event) {
  129. // search the jid associated with this stream
  130. Object.keys(self.remotessrc).forEach(function (jid) {
  131. if (self.remotessrc[jid].join('\r\n').indexOf('mslabel:' + event.stream.id) != -1) {
  132. event.peerjid = jid;
  133. }
  134. });
  135. self.remoteStreams.push(event.stream);
  136. $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
  137. };
  138. this.peerconnection.onicecandidate = function (event) {
  139. //console.log('focus onicecandidate', self.confid, new Date().getTime(), event.candidate);
  140. if (!event.candidate) {
  141. console.log('end of candidates');
  142. return;
  143. }
  144. self.sendIceCandidate(event.candidate);
  145. };
  146. this._makeConference(errorCallback);
  147. /*
  148. this.peerconnection.createOffer(
  149. function (offer) {
  150. self.peerconnection.setLocalDescription(
  151. offer,
  152. function () {
  153. // success
  154. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  155. // FIXME: could call _makeConference here and trickle candidates later
  156. self._makeConference();
  157. },
  158. function (error) {
  159. console.log('setLocalDescription failed', error);
  160. }
  161. );
  162. },
  163. function (error) {
  164. console.warn(error);
  165. }
  166. );
  167. */
  168. };
  169. // Sends a COLIBRI message which enables or disables (according to 'state') the
  170. // recording on the bridge. Waits for the result IQ and calls 'callback' with
  171. // the new recording state, according to the IQ.
  172. ColibriFocus.prototype.setRecording = function(state, token, callback) {
  173. var self = this;
  174. var elem = $iq({to: this.bridgejid, type: 'set'});
  175. elem.c('conference', {
  176. xmlns: 'http://jitsi.org/protocol/colibri',
  177. id: this.confid
  178. });
  179. elem.c('recording', {state: state, token: token});
  180. elem.up();
  181. this.connection.sendIQ(elem,
  182. function (result) {
  183. console.log('Set recording "', state, '". Result:', result);
  184. var recordingElem = $(result).find('>conference>recording');
  185. var newState = ('true' === recordingElem.attr('state'));
  186. self.recordingEnabled = newState;
  187. callback(newState);
  188. },
  189. function (error) {
  190. console.warn(error);
  191. }
  192. );
  193. };
  194. /*
  195. * Updates the display name for an endpoint with a specific jid.
  196. * jid: the jid associated with the endpoint.
  197. * displayName: the new display name for the endpoint.
  198. */
  199. ColibriFocus.prototype.setEndpointDisplayName = function(jid, displayName) {
  200. var endpointId = jid.substr(1 + jid.lastIndexOf('/'));
  201. var update = false;
  202. if (this.endpointsInfo === null) {
  203. this.endpointsInfo = {};
  204. }
  205. var endpointInfo = this.endpointsInfo[endpointId];
  206. if ('undefined' === typeof endpointInfo) {
  207. endpointInfo = this.endpointsInfo[endpointId] = {};
  208. }
  209. if (endpointInfo['displayname'] !== displayName) {
  210. endpointInfo['displayname'] = displayName;
  211. update = true;
  212. }
  213. if (update) {
  214. this.updateEndpoints();
  215. }
  216. };
  217. /*
  218. * Sends a colibri message to the bridge that contains the
  219. * current endpoints and their display names.
  220. */
  221. ColibriFocus.prototype.updateEndpoints = function() {
  222. if (this.confid === null
  223. || this.endpointsInfo === null) {
  224. return;
  225. }
  226. if (this.confid === 0) {
  227. // the colibri conference is currently initiating
  228. var self = this;
  229. window.setTimeout(function() { self.updateEndpoints()}, 1000);
  230. return;
  231. }
  232. var elem = $iq({to: this.bridgejid, type: 'set'});
  233. elem.c('conference', {
  234. xmlns: 'http://jitsi.org/protocol/colibri',
  235. id: this.confid
  236. });
  237. for (var id in this.endpointsInfo) {
  238. elem.c('endpoint');
  239. elem.attrs({ id: id,
  240. displayname: this.endpointsInfo[id]['displayname']
  241. });
  242. elem.up();
  243. }
  244. //elem.up(); //conference
  245. this.connection.sendIQ(
  246. elem,
  247. function (result) {},
  248. function (error) { console.warn(error); }
  249. );
  250. };
  251. ColibriFocus.prototype._makeConference = function (errorCallback) {
  252. var self = this;
  253. var elem = $iq({ to: this.bridgejid, type: 'get' });
  254. elem.c('conference', { xmlns: 'http://jitsi.org/protocol/colibri' });
  255. this.media.forEach(function (name) {
  256. var elemName;
  257. var elemAttrs = { initiator: 'true', expire: self.channelExpire };
  258. if ('data' === name)
  259. {
  260. elemName = 'sctpconnection';
  261. elemAttrs['port'] = 5000;
  262. }
  263. else
  264. {
  265. elemName = 'channel';
  266. if ('video' === name) {
  267. if (self.channelLastN >= 0) {
  268. elemAttrs['last-n'] = self.channelLastN;
  269. }
  270. if (config.adaptiveLastN) {
  271. elemAttrs['adaptive-last-n'] = 'true';
  272. }
  273. if (config.adaptiveSimulcast) {
  274. elemAttrs['adaptive-simulcast'] = 'true';
  275. }
  276. }
  277. }
  278. elem.c('content', { name: name });
  279. elem.c(elemName, elemAttrs);
  280. elem.attrs({ endpoint: self.myMucResource });
  281. if (config.useBundle) {
  282. elem.attrs({ 'channel-bundle-id': self.myMucResource });
  283. }
  284. elem.up();// end of channel/sctpconnection
  285. for (var j = 0; j < self.peers.length; j++) {
  286. var peer = self.peers[j];
  287. var peerEndpoint = peer.substr(1 + peer.lastIndexOf('/'));
  288. elem.c(elemName, elemAttrs);
  289. elem.attrs({ endpoint: peerEndpoint });
  290. if (config.useBundle) {
  291. elem.attrs({ 'channel-bundle-id': peerEndpoint });
  292. }
  293. elem.up(); // end of channel/sctpconnection
  294. }
  295. elem.up(); // end of content
  296. });
  297. if (this.endpointsInfo !== null) {
  298. for (var id in this.endpointsInfo) {
  299. elem.c('endpoint');
  300. elem.attrs({ id: id,
  301. displayname: this.endpointsInfo[id]['displayname']
  302. });
  303. elem.up();
  304. }
  305. }
  306. /*
  307. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  308. localSDP.media.forEach(function (media, channel) {
  309. var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
  310. elem.c('content', {name: name});
  311. elem.c('channel', {initiator: 'false', expire: self.channelExpire});
  312. // FIXME: should reuse code from .toJingle
  313. var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
  314. for (var j = 0; j < mline.fmt.length; j++) {
  315. var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
  316. elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
  317. elem.up();
  318. }
  319. localSDP.TransportToJingle(channel, elem);
  320. elem.up(); // end of channel
  321. for (j = 0; j < self.peers.length; j++) {
  322. elem.c('channel', {initiator: 'true', expire: self.channelExpire }).up();
  323. }
  324. elem.up(); // end of content
  325. });
  326. */
  327. this.connection.sendIQ(elem,
  328. function (result) {
  329. self.createdConference(result);
  330. },
  331. function (error) {
  332. console.warn(error);
  333. errorCallback(error);
  334. }
  335. );
  336. };
  337. // callback when a colibri conference was created
  338. ColibriFocus.prototype.createdConference = function (result) {
  339. console.log('created a conference on the bridge');
  340. var self = this;
  341. var tmp;
  342. this.confid = $(result).find('>conference').attr('id');
  343. var remotecontents = $(result).find('>conference>content').get();
  344. var numparticipants = 0;
  345. for (var i = 0; i < remotecontents.length; i++)
  346. {
  347. var contentName = $(remotecontents[i]).attr('name');
  348. var channelName
  349. = contentName !== 'data' ? '>channel' : '>sctpconnection';
  350. tmp = $(remotecontents[i]).find(channelName).get();
  351. this.mychannel.push($(tmp.shift()));
  352. numparticipants = tmp.length;
  353. for (j = 0; j < tmp.length; j++) {
  354. if (this.channels[j] === undefined) {
  355. this.channels[j] = [];
  356. }
  357. this.channels[j].push(tmp[j]);
  358. }
  359. }
  360. // save the 'transport' elements from 'channel-bundle'-s
  361. var channelBundles = $(result).find('>conference>channel-bundle');
  362. for (var i = 0; i < channelBundles.length; i++)
  363. {
  364. var endpointId = $(channelBundles[i]).attr('id');
  365. this.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  366. }
  367. console.log('remote channels', this.channels);
  368. // Notify that the focus has created the conference on the bridge
  369. conferenceCreated(self);
  370. var bridgeSDP = new SDP(
  371. 'v=0\r\n' +
  372. 'o=- 5151055458874951233 2 IN IP4 127.0.0.1\r\n' +
  373. 's=-\r\n' +
  374. 't=0 0\r\n' +
  375. /* Audio */
  376. (config.useBundle
  377. ? ('a=group:BUNDLE audio video' +
  378. (config.openSctp ? ' data' : '') +
  379. '\r\n')
  380. : '') +
  381. 'm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\n' +
  382. 'c=IN IP4 0.0.0.0\r\n' +
  383. 'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
  384. 'a=mid:audio\r\n' +
  385. 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' +
  386. 'a=sendrecv\r\n' +
  387. 'a=rtpmap:111 opus/48000/2\r\n' +
  388. 'a=fmtp:111 minptime=10\r\n' +
  389. 'a=rtpmap:103 ISAC/16000\r\n' +
  390. 'a=rtpmap:104 ISAC/32000\r\n' +
  391. 'a=rtpmap:0 PCMU/8000\r\n' +
  392. 'a=rtpmap:8 PCMA/8000\r\n' +
  393. 'a=rtpmap:106 CN/32000\r\n' +
  394. 'a=rtpmap:105 CN/16000\r\n' +
  395. 'a=rtpmap:13 CN/8000\r\n' +
  396. 'a=rtpmap:126 telephone-event/8000\r\n' +
  397. 'a=maxptime:60\r\n' +
  398. (config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
  399. /* Video */
  400. 'm=video 1 RTP/SAVPF 100 116 117\r\n' +
  401. 'c=IN IP4 0.0.0.0\r\n' +
  402. 'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
  403. 'a=mid:video\r\n' +
  404. 'a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n' +
  405. 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
  406. 'a=sendrecv\r\n' +
  407. 'a=rtpmap:100 VP8/90000\r\n' +
  408. 'a=rtcp-fb:100 ccm fir\r\n' +
  409. 'a=rtcp-fb:100 nack\r\n' +
  410. 'a=rtcp-fb:100 nack pli\r\n' +
  411. (config.enableFirefoxSupport? "" : 'a=rtcp-fb:100 goog-remb\r\n') +
  412. 'a=rtpmap:116 red/90000\r\n' +
  413. 'a=rtpmap:117 ulpfec/90000\r\n' +
  414. (config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
  415. /* Data SCTP */
  416. (config.openSctp ?
  417. 'm=application 1 DTLS/SCTP 5000\r\n' +
  418. 'c=IN IP4 0.0.0.0\r\n' +
  419. 'a=sctpmap:5000 webrtc-datachannel\r\n' +
  420. 'a=mid:data\r\n'
  421. : '')
  422. );
  423. bridgeSDP.media.length = this.mychannel.length;
  424. var channel;
  425. /*
  426. for (channel = 0; channel < bridgeSDP.media.length; channel++) {
  427. bridgeSDP.media[channel] = '';
  428. // unchanged lines
  429. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'm=') + '\r\n';
  430. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'c=') + '\r\n';
  431. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:')) {
  432. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:') + '\r\n';
  433. }
  434. if (SDPUtil.find_line(localSDP.media[channel], 'a=mid:')) {
  435. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=mid:') + '\r\n';
  436. }
  437. if (SDPUtil.find_line(localSDP.media[channel], 'a=sendrecv')) {
  438. bridgeSDP.media[channel] += 'a=sendrecv\r\n';
  439. }
  440. if (SDPUtil.find_line(localSDP.media[channel], 'a=extmap:')) {
  441. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=extmap:').join('\r\n') + '\r\n';
  442. }
  443. // FIXME: should look at m-line and group the ids together
  444. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtpmap:')) {
  445. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtpmap:').join('\r\n') + '\r\n';
  446. }
  447. if (SDPUtil.find_line(localSDP.media[channel], 'a=fmtp:')) {
  448. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=fmtp:').join('\r\n') + '\r\n';
  449. }
  450. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp-fb:')) {
  451. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtcp-fb:').join('\r\n') + '\r\n';
  452. }
  453. // FIXME: changed lines -- a=sendrecv direction, a=setup direction
  454. }
  455. */
  456. for (channel = 0; channel < bridgeSDP.media.length; channel++) {
  457. // get the mixed ssrc
  458. tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  459. // FIXME: check rtp-level-relay-type
  460. var name = bridgeSDP.media[channel].split(" ")[0].substr(2); // 'm=audio ...'
  461. if (name === 'audio' || name === 'video') {
  462. // make chrome happy... '3735928559' == 0xDEADBEEF
  463. var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
  464. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
  465. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
  466. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
  467. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
  468. }
  469. // FIXME: should take code from .fromJingle
  470. var channelBundleId = $(this.mychannel[channel]).attr('channel-bundle-id');
  471. if (typeof channelBundleId != 'undefined') {
  472. tmp = this.bundledTransports[channelBundleId];
  473. } else {
  474. tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  475. }
  476. if (tmp.length) {
  477. bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  478. bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  479. tmp.find('>candidate').each(function () {
  480. bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
  481. });
  482. tmp = tmp.find('>fingerprint');
  483. if (tmp.length) {
  484. bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  485. bridgeSDP.media[channel] += 'a=setup:actpass\r\n'; // offer so always actpass
  486. }
  487. }
  488. }
  489. bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
  490. var bridgeDesc = new RTCSessionDescription({type: 'offer', sdp: bridgeSDP.raw});
  491. bridgeDesc = simulcast.transformRemoteDescription(bridgeDesc);
  492. this.peerconnection.setRemoteDescription(bridgeDesc,
  493. function () {
  494. console.log('setRemoteDescription success');
  495. self.peerconnection.createAnswer(
  496. function (answer) {
  497. self.peerconnection.setLocalDescription(answer,
  498. function () {
  499. console.log('setLocalDescription succeeded.');
  500. // make sure our presence is updated
  501. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  502. var localSDP = new SDP(self.peerconnection.localDescription.sdp);
  503. self.updateLocalChannel(localSDP);
  504. // now initiate sessions
  505. for (var i = 0; i < numparticipants; i++) {
  506. self.initiate(self.peers[i], true);
  507. }
  508. // Notify we've created the conference
  509. conferenceCreated(self);
  510. },
  511. function (error) {
  512. console.warn('setLocalDescription failed.', error);
  513. }
  514. );
  515. },
  516. function (error) {
  517. console.warn('createAnswer failed.', error);
  518. }
  519. );
  520. /*
  521. for (var i = 0; i < numparticipants; i++) {
  522. self.initiate(self.peers[i], true);
  523. }
  524. */
  525. },
  526. function (error) {
  527. console.log('setRemoteDescription failed.', error);
  528. }
  529. );
  530. };
  531. ColibriFocus.prototype.updateLocalChannel = function(localSDP, parts) {
  532. var self = this;
  533. var elem = $iq({to: self.bridgejid, type: 'get'});
  534. elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
  535. localSDP.media.forEach(function (media, channel) {
  536. var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
  537. elem.c('content', {name: name});
  538. var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
  539. if (name !== 'data') {
  540. elem.c('channel', {
  541. initiator: 'true',
  542. expire: self.channelExpire,
  543. id: self.mychannel[channel].attr('id'),
  544. endpoint: self.myMucResource
  545. });
  546. if (!parts || parts.indexOf('sources') !== -1) {
  547. // signal (through COLIBRI) to the bridge
  548. // the SSRC groups of the participant
  549. // that plays the role of the focus
  550. var ssrc_group_lines = SDPUtil.find_lines(media, 'a=ssrc-group:');
  551. var idx = 0;
  552. var hasSIM = false;
  553. ssrc_group_lines.forEach(function (line) {
  554. idx = line.indexOf(' ');
  555. var semantics = line.substr(0, idx).substr(13);
  556. var ssrcs = line.substr(14 + semantics.length).split(' ');
  557. if (ssrcs.length != 0) {
  558. elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
  559. ssrcs.forEach(function (ssrc) {
  560. elem.c('source', { ssrc: ssrc })
  561. .up();
  562. });
  563. elem.up();
  564. }
  565. });
  566. if (!hasSIM && name == 'video') {
  567. // disable simulcast with an empty ssrc-group element.
  568. elem.c('ssrc-group', { semantics: 'SIM', xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
  569. elem.up();
  570. }
  571. }
  572. if (!parts || parts.indexOf('payload-type') !== -1) {
  573. // FIXME: should reuse code from .toJingle
  574. for (var j = 0; j < mline.fmt.length; j++) {
  575. var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
  576. if (rtpmap) {
  577. elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
  578. elem.up();
  579. }
  580. }
  581. }
  582. }
  583. else
  584. {
  585. var sctpmap = SDPUtil.find_line(media, 'a=sctpmap:' + mline.fmt[0]);
  586. var sctpPort = SDPUtil.parse_sctpmap(sctpmap)[0];
  587. elem.c("sctpconnection",
  588. {
  589. initiator: 'true',
  590. expire: self.channelExpire,
  591. id: self.mychannel[channel].attr('id'),
  592. endpoint: self.myMucResource,
  593. port: sctpPort
  594. }
  595. );
  596. }
  597. if (!parts || parts.indexOf('transport') !== -1) {
  598. localSDP.TransportToJingle(channel, elem);
  599. }
  600. elem.up(); // end of channel
  601. elem.up(); // end of content
  602. });
  603. self.connection.sendIQ(elem,
  604. function (result) {
  605. // ...
  606. },
  607. function (error) {
  608. console.error(
  609. "ERROR sending colibri message",
  610. error, elem);
  611. }
  612. );
  613. };
  614. // send a session-initiate to a new participant
  615. ColibriFocus.prototype.initiate = function (peer, isInitiator) {
  616. var participant = this.peers.indexOf(peer);
  617. console.log('tell', peer, participant);
  618. var sdp;
  619. if (!(this.peerconnection !== null && this.peerconnection.signalingState == 'stable')) {
  620. console.error('can not initiate a new session without a stable peerconnection');
  621. return;
  622. }
  623. sdp = new SDP(this.peerconnection.remoteDescription.sdp);
  624. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  625. // throw away stuff we don't want
  626. // not needed with static offer
  627. if (!config.useBundle) {
  628. sdp.removeSessionLines('a=group:');
  629. }
  630. sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
  631. for (var i = 0; i < sdp.media.length; i++) {
  632. if (!config.useRtcpMux) {
  633. sdp.removeMediaLines(i, 'a=rtcp-mux');
  634. }
  635. sdp.removeMediaLines(i, 'a=ssrc:');
  636. sdp.removeMediaLines(i, 'a=ssrc-group:');
  637. sdp.removeMediaLines(i, 'a=crypto:');
  638. sdp.removeMediaLines(i, 'a=candidate:');
  639. sdp.removeMediaLines(i, 'a=ice-options:google-ice');
  640. sdp.removeMediaLines(i, 'a=ice-ufrag:');
  641. sdp.removeMediaLines(i, 'a=ice-pwd:');
  642. sdp.removeMediaLines(i, 'a=fingerprint:');
  643. sdp.removeMediaLines(i, 'a=setup:');
  644. }
  645. // add stuff we got from the bridge
  646. for (var j = 0; j < sdp.media.length; j++) {
  647. var chan = $(this.channels[participant][j]);
  648. console.log('channel id', chan.attr('id'));
  649. tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  650. var name = sdp.media[j].split(" ")[0].substr(2); // 'm=audio ...'
  651. if (name === 'audio' || name === 'video') {
  652. // make chrome happy... '3735928559' == 0xDEADBEEF
  653. var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
  654. sdp.media[j] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
  655. sdp.media[j] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
  656. sdp.media[j] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
  657. sdp.media[j] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
  658. }
  659. // In the case of bundle, we add each candidate to all m= lines/jingle contents,
  660. // just as chrome does
  661. if (config.useBundle){
  662. tmp = this.bundledTransports[chan.attr('channel-bundle-id')];
  663. } else {
  664. tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  665. }
  666. if (tmp.length) {
  667. if (tmp.attr('ufrag'))
  668. sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  669. if (tmp.attr('pwd'))
  670. sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  671. // and the candidates...
  672. tmp.find('>candidate').each(function () {
  673. sdp.media[j] += SDPUtil.candidateFromJingle(this);
  674. });
  675. tmp = tmp.find('>fingerprint');
  676. if (tmp.length) {
  677. sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  678. /*
  679. if (tmp.attr('direction')) {
  680. sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
  681. }
  682. */
  683. sdp.media[j] += 'a=setup:actpass\r\n';
  684. }
  685. }
  686. }
  687. for (var i = 0; i < sdp.media.length; i++) {
  688. // re-add all remote a=ssrcs _and_ a=ssrc-group
  689. for (var jid in this.remotessrc) {
  690. if (jid == peer || !this.remotessrc[jid][i])
  691. continue;
  692. sdp.media[i] += this.remotessrc[jid][i];
  693. }
  694. // add local a=ssrc-group: lines
  695. lines = SDPUtil.find_lines(localSDP.media[i], 'a=ssrc-group:');
  696. if (lines.length != 0)
  697. sdp.media[i] += lines.join('\r\n') + '\r\n';
  698. // and local a=ssrc: lines
  699. sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc:').join('\r\n') + '\r\n';
  700. }
  701. sdp.raw = sdp.session + sdp.media.join('');
  702. // make a new colibri session and configure it
  703. // FIXME: is it correct to use this.connection.jid when used in a MUC?
  704. var sess = new ColibriSession(this.connection.jid,
  705. Math.random().toString(36).substr(2, 12), // random string
  706. this.connection);
  707. sess.initiate(peer);
  708. sess.colibri = this;
  709. // We do not announce our audio per conference peer, so only video is set here
  710. sess.localVideo = this.connection.jingle.localVideo;
  711. sess.media_constraints = this.connection.jingle.media_constraints;
  712. sess.pc_constraints = this.connection.jingle.pc_constraints;
  713. sess.ice_config = this.connection.jingle.ice_config;
  714. this.connection.jingle.sessions[sess.sid] = sess;
  715. this.connection.jingle.jid2session[sess.peerjid] = sess;
  716. // send a session-initiate
  717. var init = $iq({to: peer, type: 'set'})
  718. .c('jingle',
  719. {xmlns: 'urn:xmpp:jingle:1',
  720. action: 'session-initiate',
  721. initiator: sess.me,
  722. sid: sess.sid
  723. }
  724. );
  725. sdp.toJingle(init, 'initiator');
  726. this.connection.sendIQ(init,
  727. function (res) {
  728. console.log('got result');
  729. },
  730. function (err) {
  731. console.log('got error');
  732. }
  733. );
  734. };
  735. // pull in a new participant into the conference
  736. ColibriFocus.prototype.addNewParticipant = function (peer) {
  737. var self = this;
  738. if (this.confid === 0 || !this.peerconnection.localDescription)
  739. {
  740. // bad state
  741. if (this.confid === 0)
  742. {
  743. console.error('confid does not exist yet, postponing', peer);
  744. }
  745. else
  746. {
  747. console.error('local description not ready yet, postponing', peer);
  748. }
  749. window.setTimeout(function () { self.addNewParticipant(peer); }, 250);
  750. return;
  751. }
  752. var index = this.channels.length;
  753. this.channels.push([]);
  754. this.peers.push(peer);
  755. var elem = $iq({to: this.bridgejid, type: 'get'});
  756. elem.c(
  757. 'conference',
  758. { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
  759. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  760. localSDP.media.forEach(function (media, channel) {
  761. var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
  762. var elemName;
  763. var endpointId = peer.substr(1 + peer.lastIndexOf('/'));
  764. var elemAttrs
  765. = {
  766. initiator: 'true',
  767. expire: self.channelExpire,
  768. endpoint: endpointId
  769. };
  770. if (config.useBundle) {
  771. elemAttrs['channel-bundle-id'] = endpointId;
  772. }
  773. if ('data' == name)
  774. {
  775. elemName = 'sctpconnection';
  776. elemAttrs['port'] = 5000;
  777. }
  778. else
  779. {
  780. elemName = 'channel';
  781. if ('video' === name) {
  782. if (self.channelLastN >= 0) {
  783. elemAttrs['last-n'] = self.channelLastN;
  784. }
  785. if (config.adaptiveLastN) {
  786. elemAttrs['adaptive-last-n'] = 'true';
  787. }
  788. if (config.adaptiveSimulcast) {
  789. elemAttrs['adaptive-simulcast'] = 'true';
  790. }
  791. }
  792. }
  793. elem.c('content', { name: name });
  794. elem.c(elemName, elemAttrs);
  795. elem.up(); // end of channel/sctpconnection
  796. elem.up(); // end of content
  797. });
  798. this.connection.sendIQ(elem,
  799. function (result) {
  800. var contents = $(result).find('>conference>content').get();
  801. var i;
  802. for (i = 0; i < contents.length; i++) {
  803. var channelXml = $(contents[i]).find('>channel');
  804. if (channelXml.length)
  805. {
  806. tmp = channelXml.get();
  807. }
  808. else
  809. {
  810. tmp = $(contents[i]).find('>sctpconnection').get();
  811. }
  812. self.channels[index][i] = tmp[0];
  813. }
  814. var channelBundles = $(result).find('>conference>channel-bundle');
  815. for (i = 0; i < channelBundles.length; i++)
  816. {
  817. var endpointId = $(channelBundles[i]).attr('id');
  818. self.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  819. }
  820. self.initiate(peer, true);
  821. },
  822. function (error) {
  823. console.warn(error);
  824. }
  825. );
  826. };
  827. // update the channel description (payload-types + dtls fp) for a participant
  828. ColibriFocus.prototype.updateRemoteChannel = function (remoteSDP, participant, parts) {
  829. console.log('change allocation for', this.confid);
  830. var self = this;
  831. var change = $iq({to: this.bridgejid, type: 'set'});
  832. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  833. for (channel = 0; channel < this.channels[participant].length; channel++) {
  834. if (!remoteSDP.media[channel])
  835. continue;
  836. var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
  837. change.c('content', {name: name});
  838. if (name !== 'data') {
  839. change.c('channel', {
  840. id: $(this.channels[participant][channel]).attr('id'),
  841. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  842. expire: self.channelExpire
  843. });
  844. if (!parts || parts.indexOf('sources') !== -1) {
  845. // signal (throught COLIBRI) to the bridge the SSRC groups of this
  846. // participant
  847. var ssrc_group_lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
  848. var idx = 0;
  849. var hasSIM = false;
  850. ssrc_group_lines.forEach(function (line) {
  851. idx = line.indexOf(' ');
  852. var semantics = line.substr(0, idx).substr(13);
  853. if (semantics == 'SIM') {
  854. hasSIM = true;
  855. }
  856. var ssrcs = line.substr(14 + semantics.length).split(' ');
  857. if (ssrcs.length != 0) {
  858. change.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
  859. ssrcs.forEach(function (ssrc) {
  860. change.c('source', { ssrc: ssrc })
  861. .up();
  862. });
  863. change.up();
  864. }
  865. });
  866. if (!hasSIM && name == 'video') {
  867. // disable simulcast with an empty ssrc-group element.
  868. change.c('ssrc-group', { semantics: 'SIM', xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
  869. change.up();
  870. }
  871. }
  872. if (!parts || parts.indexOf('payload-type') !== -1) {
  873. var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
  874. rtpmap.forEach(function (val) {
  875. // TODO: too much copy-paste
  876. var rtpmap = SDPUtil.parse_rtpmap(val);
  877. change.c('payload-type', rtpmap);
  878. //
  879. // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
  880. /*
  881. if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
  882. tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
  883. for (var k = 0; k < tmp.length; k++) {
  884. change.c('parameter', tmp[k]).up();
  885. }
  886. }
  887. */
  888. change.up();
  889. });
  890. }
  891. }
  892. else
  893. {
  894. var sctpmap = SDPUtil.find_line(remoteSDP.media[channel], 'a=sctpmap:');
  895. change.c('sctpconnection', {
  896. id: $(this.channels[participant][channel]).attr('id'),
  897. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  898. expire: self.channelExpire,
  899. port: SDPUtil.parse_sctpmap(sctpmap)[0]
  900. });
  901. }
  902. if (!parts || parts.indexOf('transport') !== -1) {
  903. // now add transport
  904. remoteSDP.TransportToJingle(channel, change);
  905. }
  906. change.up(); // end of channel/sctpconnection
  907. change.up(); // end of content
  908. }
  909. this.connection.sendIQ(change,
  910. function (res) {
  911. console.log('got result');
  912. },
  913. function (err) {
  914. console.log('got error');
  915. }
  916. );
  917. };
  918. /**
  919. * Switches video streams.
  920. * @param new_stream new stream that will be used as video of this session.
  921. * @param oldStream old video stream of this session.
  922. * @param success_callback callback executed after successful stream switch.
  923. */
  924. ColibriFocus.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
  925. var self = this;
  926. // Stop the stream to trigger onended event for old stream
  927. oldStream.stop();
  928. // Remember SDP to figure out added/removed SSRCs
  929. var oldSdp = null;
  930. if(self.peerconnection) {
  931. if(self.peerconnection.localDescription) {
  932. oldSdp = new SDP(self.peerconnection.localDescription.sdp);
  933. }
  934. self.peerconnection.removeStream(oldStream);
  935. self.peerconnection.addStream(new_stream);
  936. }
  937. self.connection.jingle.localVideo = new_stream;
  938. self.connection.jingle.localStreams = [];
  939. self.connection.jingle.localStreams.push(self.connection.jingle.localAudio);
  940. self.connection.jingle.localStreams.push(self.connection.jingle.localVideo);
  941. // Conference is not active
  942. if(!oldSdp || !self.peerconnection) {
  943. success_callback();
  944. return;
  945. }
  946. self.peerconnection.switchstreams = true;
  947. self.modifySources(function() {
  948. console.log('modify sources done');
  949. var newSdp = new SDP(self.peerconnection.localDescription.sdp);
  950. // change allocation on bridge
  951. self.updateLocalChannel(newSdp, ['sources']);
  952. console.log("SDPs", oldSdp, newSdp);
  953. self.notifyMySSRCUpdate(oldSdp, newSdp);
  954. success_callback();
  955. });
  956. };
  957. // tell everyone about a new participants a=ssrc lines (isadd is true)
  958. // or a leaving participants a=ssrc lines
  959. ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd) {
  960. var self = this;
  961. this.peers.forEach(function (peerjid) {
  962. if (peerjid == fromJid) return;
  963. console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', fromJid);
  964. if (!self.remotessrc[peerjid]) {
  965. // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
  966. // possibly, this.remoteSSRC[session.peerjid] does not exist yet
  967. console.warn('do we really want to bother', peerjid, 'with updates yet?');
  968. }
  969. var peersess = self.connection.jingle.jid2session[peerjid];
  970. if(!peersess){
  971. console.warn('no session with peer: '+peerjid+' yet...');
  972. return;
  973. }
  974. self.sendSSRCUpdateIq(sdpMediaSsrcs, peersess.sid, peersess.initiator, peerjid, isadd);
  975. });
  976. };
  977. /**
  978. * Overrides SessionBase.addSource.
  979. *
  980. * @param elem proprietary 'add source' Jingle request(XML node).
  981. * @param fromJid JID of the participant to whom new ssrcs belong.
  982. */
  983. ColibriFocus.prototype.addSource = function (elem, fromJid) {
  984. var self = this;
  985. // FIXME: dirty waiting
  986. if (!this.peerconnection.localDescription)
  987. {
  988. console.warn("addSource - localDescription not ready yet");
  989. setTimeout(function() { self.addSource(elem, fromJid); }, 200);
  990. return;
  991. }
  992. this.peerconnection.addSource(elem);
  993. // NOTE(gp) this could be a useful thing to have in every Array object.
  994. var diffArray = function(a) {
  995. return this.filter(function(i) {return a.indexOf(i) < 0;});
  996. };
  997. var peerSsrc = this.remotessrc[fromJid];
  998. // console.log("On ADD", this.peerconnection.addssrc, peerSsrc);
  999. this.peerconnection.addssrc.forEach(function(val, idx){
  1000. if(!peerSsrc[idx]){
  1001. // add ssrc
  1002. peerSsrc[idx] = val;
  1003. } else if (val) {
  1004. // NOTE(gp) we can't expect the lines in the removessrc SDP fragment
  1005. // to be in the same order as in the lines in the peerSsrc SDP
  1006. // fragment. So, here we remove the val lines and re-add them.
  1007. var lines = peerSsrc[idx].split('\r\n');
  1008. var diffLines = val.split('\r\n');
  1009. // Remove ssrc
  1010. peerSsrc[idx] = diffArray.apply(lines, [diffLines]).join('\r\n');
  1011. // Add ssrc
  1012. peerSsrc[idx] = peerSsrc[idx]+val;
  1013. }
  1014. });
  1015. var oldRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
  1016. this.modifySources(function() {
  1017. // Notify other participants about added ssrc
  1018. var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
  1019. var newSSRCs = oldRemoteSdp.getNewMedia(remoteSDP);
  1020. self.sendSSRCUpdate(newSSRCs, fromJid, true);
  1021. // change allocation on bridge
  1022. if (peerSsrc[1] /* video */) {
  1023. // If the remote peer has changed its video sources, then we need to
  1024. // update the bridge with this information, in order for the
  1025. // simulcast manager of the remote peer to update its layers, and
  1026. // any associated receivers to adjust to the change.
  1027. var videoSDP = new SDP(['v=0', 'm=audio', 'a=mid:audio', peerSsrc[0]].join('\r\n') + ['m=video', 'a=mid:video', peerSsrc[1]].join('\r\n'));
  1028. var participant = self.peers.indexOf(fromJid);
  1029. self.updateRemoteChannel(videoSDP, participant, ['sources']);
  1030. }
  1031. });
  1032. };
  1033. /**
  1034. * Overrides SessionBase.removeSource.
  1035. *
  1036. * @param elem proprietary 'remove source' Jingle request(XML node).
  1037. * @param fromJid JID of the participant to whom removed ssrcs belong.
  1038. */
  1039. ColibriFocus.prototype.removeSource = function (elem, fromJid) {
  1040. var self = this;
  1041. // FIXME: dirty waiting
  1042. if (!self.peerconnection.localDescription)
  1043. {
  1044. console.warn("removeSource - localDescription not ready yet");
  1045. setTimeout(function() { self.removeSource(elem, fromJid); }, 200);
  1046. return;
  1047. }
  1048. this.peerconnection.removeSource(elem);
  1049. // NOTE(gp) this could be a useful thing to have in every Array object.
  1050. var diffArray = function(a) {
  1051. return this.filter(function(i) {return a.indexOf(i) < 0;});
  1052. };
  1053. var peerSsrc = this.remotessrc[fromJid];
  1054. // console.log("On REMOVE", this.peerconnection.removessrc, peerSsrc);
  1055. this.peerconnection.removessrc.forEach(function(val, idx){
  1056. if(peerSsrc[idx] && val){
  1057. // NOTE(gp) we can't expect the lines in the removessrc SDP fragment
  1058. // to be in the same order as in the lines in the peerSsrc SDP
  1059. // fragment.
  1060. var lines = peerSsrc[idx].split('\r\n');
  1061. var diffLines = val.split('\r\n');
  1062. // Remove ssrc
  1063. peerSsrc[idx] = diffArray.apply(lines, [diffLines]).join('\r\n');
  1064. }
  1065. });
  1066. var oldSDP = new SDP(self.peerconnection.remoteDescription.sdp);
  1067. this.modifySources(function(){
  1068. // Notify other participants about removed ssrc
  1069. var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
  1070. var removedSSRCs = remoteSDP.getNewMedia(oldSDP);
  1071. self.sendSSRCUpdate(removedSSRCs, fromJid, false);
  1072. // change allocation on bridge
  1073. if (peerSsrc[1] /* video */) {
  1074. // If the remote peer has changed its video sources, then we need to
  1075. // update the bridge with this information, in order for the
  1076. // simulcast manager of the remote peer to update its layers, and
  1077. // any associated receivers to adjust to the change.
  1078. var videoSDP = new SDP(['v=0', 'm=audio', 'a=mid:audio', peerSsrc[0]].join('\r\n') + ['m=video', 'a=mid:video', peerSsrc[1]].join('\r\n'));
  1079. var participant = self.peers.indexOf(fromJid);
  1080. self.updateRemoteChannel(videoSDP, participant, ['sources']);
  1081. }
  1082. });
  1083. };
  1084. ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
  1085. var participant = this.peers.indexOf(session.peerjid);
  1086. console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
  1087. var remoteSDP = new SDP('');
  1088. var channel;
  1089. remoteSDP.fromJingle(elem);
  1090. // ACT 1: change allocation on bridge
  1091. this.updateRemoteChannel(remoteSDP, participant);
  1092. // ACT 2: tell anyone else about the new SSRCs
  1093. this.sendSSRCUpdate(remoteSDP.getMediaSsrcMap(), session.peerjid, true);
  1094. // ACT 3: note the SSRCs
  1095. this.remotessrc[session.peerjid] = [];
  1096. for (channel = 0; channel < this.channels[participant].length; channel++) {
  1097. //if (channel == 0) continue; FIXME: does not work as intended
  1098. if (!remoteSDP.media[channel])
  1099. continue;
  1100. var lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
  1101. if (lines.length != 0)
  1102. // prepend ssrc-groups
  1103. this.remotessrc[session.peerjid][channel] = lines.join('\r\n') + '\r\n';
  1104. if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
  1105. {
  1106. if (!this.remotessrc[session.peerjid][channel])
  1107. this.remotessrc[session.peerjid][channel] = '';
  1108. this.remotessrc[session.peerjid][channel] +=
  1109. SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
  1110. .join('\r\n') + '\r\n';
  1111. }
  1112. }
  1113. // ACT 4: add new a=ssrc and s=ssrc-group lines to local remotedescription
  1114. for (channel = 0; channel < this.channels[participant].length; channel++) {
  1115. //if (channel == 0) continue; FIXME: does not work as intended
  1116. if (!remoteSDP.media[channel])
  1117. continue;
  1118. var lines = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:');
  1119. if (lines.length != 0)
  1120. this.peerconnection.enqueueAddSsrc(
  1121. channel, SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc-group:').join('\r\n') + '\r\n');
  1122. if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
  1123. this.peerconnection.enqueueAddSsrc(
  1124. channel,
  1125. SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'
  1126. );
  1127. }
  1128. }
  1129. this.modifySources();
  1130. };
  1131. // relay ice candidates to bridge using trickle
  1132. ColibriFocus.prototype.addIceCandidate = function (session, elem) {
  1133. var self = this;
  1134. var participant = this.peers.indexOf(session.peerjid);
  1135. //console.log('change transport allocation for', this.confid, session.peerjid, participant);
  1136. var change = $iq({to: this.bridgejid, type: 'set'});
  1137. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  1138. $(elem).each(function () {
  1139. var name = $(this).attr('name');
  1140. // If we are using bundle, audio/video/data channel will have the same candidates, so only send them for
  1141. // the audio channel.
  1142. if (config.useBundle && name !== 'audio') {
  1143. return;
  1144. }
  1145. var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
  1146. if (name != 'audio' && name != 'video')
  1147. channel = 2; // name == 'data'
  1148. change.c('content', {name: name});
  1149. if (name !== 'data')
  1150. {
  1151. change.c('channel', {
  1152. id: $(self.channels[participant][channel]).attr('id'),
  1153. endpoint: $(self.channels[participant][channel]).attr('endpoint'),
  1154. expire: self.channelExpire
  1155. });
  1156. }
  1157. else
  1158. {
  1159. change.c('sctpconnection', {
  1160. id: $(self.channels[participant][channel]).attr('id'),
  1161. endpoint: $(self.channels[participant][channel]).attr('endpoint'),
  1162. expire: self.channelExpire
  1163. });
  1164. }
  1165. $(this).find('>transport').each(function () {
  1166. change.c('transport', {
  1167. ufrag: $(this).attr('ufrag'),
  1168. pwd: $(this).attr('pwd'),
  1169. xmlns: $(this).attr('xmlns')
  1170. });
  1171. if (config.useRtcpMux
  1172. && 'channel' === change.node.parentNode.nodeName) {
  1173. change.c('rtcp-mux').up();
  1174. }
  1175. $(this).find('>candidate').each(function () {
  1176. /* not yet
  1177. if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
  1178. // chrome generates TCP candidates with port 0
  1179. return;
  1180. }
  1181. */
  1182. var line = SDPUtil.candidateFromJingle(this);
  1183. change.c('candidate', SDPUtil.candidateToJingle(line)).up();
  1184. });
  1185. change.up(); // end of transport
  1186. });
  1187. change.up(); // end of channel/sctpconnection
  1188. change.up(); // end of content
  1189. });
  1190. // FIXME: need to check if there is at least one candidate when filtering TCP ones
  1191. this.connection.sendIQ(change,
  1192. function (res) {
  1193. console.log('got result');
  1194. },
  1195. function (err) {
  1196. console.error('got error', err);
  1197. }
  1198. );
  1199. };
  1200. // send our own candidate to the bridge
  1201. ColibriFocus.prototype.sendIceCandidate = function (candidate) {
  1202. var self = this;
  1203. //console.log('candidate', candidate);
  1204. if (!candidate) {
  1205. console.log('end of candidates');
  1206. return;
  1207. }
  1208. if (this.drip_container.length === 0) {
  1209. // start 20ms callout
  1210. window.setTimeout(
  1211. function () {
  1212. if (self.drip_container.length === 0) return;
  1213. self.sendIceCandidates(self.drip_container);
  1214. self.drip_container = [];
  1215. },
  1216. 20);
  1217. }
  1218. this.drip_container.push(candidate);
  1219. };
  1220. // sort and send multiple candidates
  1221. ColibriFocus.prototype.sendIceCandidates = function (candidates) {
  1222. var self = this;
  1223. var mycands = $iq({to: this.bridgejid, type: 'set'});
  1224. mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  1225. // FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
  1226. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  1227. for (var mid = 0; mid < localSDP.media.length; mid++)
  1228. {
  1229. var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
  1230. if (cands.length > 0)
  1231. {
  1232. var name = cands[0].sdpMid;
  1233. mycands.c('content', {name: name });
  1234. if (name !== 'data')
  1235. {
  1236. mycands.c('channel', {
  1237. id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
  1238. endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
  1239. expire: self.channelExpire
  1240. });
  1241. }
  1242. else
  1243. {
  1244. mycands.c('sctpconnection', {
  1245. id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
  1246. endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
  1247. port: $(this.mychannel[cands[0].sdpMLineIndex]).attr('port'),
  1248. expire: self.channelExpire
  1249. });
  1250. }
  1251. mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
  1252. if (config.useRtcpMux && name !== 'data') {
  1253. mycands.c('rtcp-mux').up();
  1254. }
  1255. for (var i = 0; i < cands.length; i++) {
  1256. mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
  1257. }
  1258. mycands.up(); // transport
  1259. mycands.up(); // channel / sctpconnection
  1260. mycands.up(); // content
  1261. }
  1262. }
  1263. console.log('send cands', candidates);
  1264. this.connection.sendIQ(mycands,
  1265. function (res) {
  1266. console.log('got result');
  1267. },
  1268. function (err) {
  1269. console.error('got error', err);
  1270. }
  1271. );
  1272. };
  1273. ColibriFocus.prototype.terminate = function (session, reason) {
  1274. console.log('remote session terminated from', session.peerjid);
  1275. var participant = this.peers.indexOf(session.peerjid);
  1276. if (!this.remotessrc[session.peerjid] || participant == -1) {
  1277. return;
  1278. }
  1279. var ssrcs = this.remotessrc[session.peerjid];
  1280. for (var i = 0; i < ssrcs.length; i++) {
  1281. this.peerconnection.enqueueRemoveSsrc(i, ssrcs[i]);
  1282. }
  1283. // remove from this.peers
  1284. this.peers.splice(participant, 1);
  1285. // expire channel on bridge
  1286. var change = $iq({to: this.bridgejid, type: 'set'});
  1287. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  1288. for (var channel = 0; channel < this.channels[participant].length; channel++) {
  1289. var name = channel === 0 ? 'audio' : 'video';
  1290. if (channel == 2)
  1291. name = 'data';
  1292. change.c('content', {name: name});
  1293. if (name !== 'data')
  1294. {
  1295. change.c('channel', {
  1296. id: $(this.channels[participant][channel]).attr('id'),
  1297. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  1298. expire: '0'
  1299. });
  1300. }
  1301. else
  1302. {
  1303. change.c('sctpconnection', {
  1304. id: $(this.channels[participant][channel]).attr('id'),
  1305. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  1306. expire: '0'
  1307. });
  1308. }
  1309. change.up(); // end of channel/sctpconnection
  1310. change.up(); // end of content
  1311. }
  1312. this.connection.sendIQ(change,
  1313. function (res) {
  1314. console.log('got result');
  1315. },
  1316. function (err) {
  1317. console.error('got error', err);
  1318. }
  1319. );
  1320. // and remove from channels
  1321. this.channels.splice(participant, 1);
  1322. // tell everyone about the ssrcs to be removed
  1323. var sdp = new SDP('');
  1324. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  1325. var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
  1326. for (var j = 0; j < ssrcs.length; j++) {
  1327. sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
  1328. sdp.media[j] += ssrcs[j];
  1329. this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]);
  1330. }
  1331. this.sendSSRCUpdate(sdp.getMediaSsrcMap(), session.peerjid, false);
  1332. delete this.remotessrc[session.peerjid];
  1333. this.modifySources();
  1334. };
  1335. ColibriFocus.prototype.sendTerminate = function (session, reason, text) {
  1336. var term = $iq({to: session.peerjid, type: 'set'})
  1337. .c('jingle',
  1338. {xmlns: 'urn:xmpp:jingle:1',
  1339. action: 'session-terminate',
  1340. initiator: session.me,
  1341. sid: session.sid})
  1342. .c('reason')
  1343. .c(reason || 'success');
  1344. if (text) {
  1345. term.up().c('text').t(text);
  1346. }
  1347. this.connection.sendIQ(term,
  1348. function () {
  1349. if (!session)
  1350. return;
  1351. if (session.peerconnection) {
  1352. session.peerconnection.close();
  1353. session.peerconnection = null;
  1354. }
  1355. session.terminate();
  1356. var ack = {};
  1357. ack.source = 'terminate';
  1358. $(document).trigger('ack.jingle', [session.sid, ack]);
  1359. },
  1360. function (stanza) {
  1361. var error = ($(stanza).find('error').length) ? {
  1362. code: $(stanza).find('error').attr('code'),
  1363. reason: $(stanza).find('error :first')[0].tagName,
  1364. }:{};
  1365. $(document).trigger('ack.jingle', [self.sid, error]);
  1366. },
  1367. 10000);
  1368. if (this.statsinterval !== null) {
  1369. window.clearInterval(this.statsinterval);
  1370. this.statsinterval = null;
  1371. }
  1372. };
  1373. ColibriFocus.prototype.setRTCPTerminationStrategy = function (strategyFQN) {
  1374. var self = this;
  1375. // TODO(gp) maybe move the RTCP termination strategy element under the
  1376. // content or channel element.
  1377. var strategyIQ = $iq({to: this.bridgejid, type: 'set'});
  1378. strategyIQ.c('conference', {
  1379. xmlns: 'http://jitsi.org/protocol/colibri',
  1380. id: this.confid,
  1381. });
  1382. strategyIQ.c('rtcp-termination-strategy', {name: strategyFQN });
  1383. strategyIQ.c('content', {name: "video"});
  1384. strategyIQ.up(); // end of content
  1385. console.log('setting RTCP termination strategy', strategyFQN);
  1386. this.connection.sendIQ(strategyIQ,
  1387. function (res) {
  1388. console.log('got result');
  1389. },
  1390. function (err) {
  1391. console.error('got error', err);
  1392. }
  1393. );
  1394. };
  1395. /**
  1396. * Sets the default value of the channel last-n attribute in this conference and
  1397. * updates/patches the existing channels.
  1398. */
  1399. ColibriFocus.prototype.setChannelLastN = function (channelLastN) {
  1400. if (('number' === typeof(channelLastN))
  1401. && (this.channelLastN !== channelLastN))
  1402. {
  1403. this.channelLastN = channelLastN;
  1404. // Update/patch the existing channels.
  1405. var patch = $iq({ to: this.bridgejid, type: 'set' });
  1406. patch.c(
  1407. 'conference',
  1408. { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
  1409. patch.c('content', { name: 'video' });
  1410. patch.c(
  1411. 'channel',
  1412. {
  1413. id: $(this.mychannel[1 /* video */]).attr('id'),
  1414. 'last-n': this.channelLastN
  1415. });
  1416. patch.up(); // end of channel
  1417. for (var p = 0; p < this.channels.length; p++)
  1418. {
  1419. patch.c(
  1420. 'channel',
  1421. {
  1422. id: $(this.channels[p][1 /* video */]).attr('id'),
  1423. 'last-n': this.channelLastN
  1424. });
  1425. patch.up(); // end of channel
  1426. }
  1427. this.connection.sendIQ(
  1428. patch,
  1429. function (res) {
  1430. console.info('Set channel last-n succeeded:', res);
  1431. },
  1432. function (err) {
  1433. console.error('Set channel last-n failed:', err);
  1434. });
  1435. }
  1436. };
  1437. /**
  1438. * Sets the default value of the channel simulcast layer attribute in this
  1439. * conference and updates/patches the existing channels.
  1440. */
  1441. ColibriFocus.prototype.setReceiveSimulcastLayer = function (receiveSimulcastLayer) {
  1442. if (('number' === typeof(receiveSimulcastLayer))
  1443. && (this.receiveSimulcastLayer !== receiveSimulcastLayer))
  1444. {
  1445. // TODO(gp) be able to set the receiving simulcast layer on a per
  1446. // sender basis.
  1447. this.receiveSimulcastLayer = receiveSimulcastLayer;
  1448. // Update/patch the existing channels.
  1449. var patch = $iq({ to: this.bridgejid, type: 'set' });
  1450. patch.c(
  1451. 'conference',
  1452. { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
  1453. patch.c('content', { name: 'video' });
  1454. patch.c(
  1455. 'channel',
  1456. {
  1457. id: $(this.mychannel[1 /* video */]).attr('id'),
  1458. 'receive-simulcast-layer': this.receiveSimulcastLayer
  1459. });
  1460. patch.up(); // end of channel
  1461. for (var p = 0; p < this.channels.length; p++)
  1462. {
  1463. patch.c(
  1464. 'channel',
  1465. {
  1466. id: $(this.channels[p][1 /* video */]).attr('id'),
  1467. 'receive-simulcast-layer': this.receiveSimulcastLayer
  1468. });
  1469. patch.up(); // end of channel
  1470. }
  1471. this.connection.sendIQ(
  1472. patch,
  1473. function (res) {
  1474. console.info('Set channel simulcast receive layer succeeded:', res);
  1475. },
  1476. function (err) {
  1477. console.error('Set channel simulcast receive layer failed:', err);
  1478. });
  1479. }
  1480. };