Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

colibri.focus.js 51KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381
  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.confid = null;
  40. /**
  41. * Local XMPP resource used to join the multi user chat.
  42. * @type {*}
  43. */
  44. this.myMucResource = Strophe.getResourceFromJid(connection.emuc.myroomjid);
  45. /**
  46. * Default channel expire value in seconds.
  47. * @type {number}
  48. */
  49. this.channelExpire
  50. = ('number' === typeof(config.channelExpire))
  51. ? config.channelExpire
  52. : 15;
  53. /**
  54. * Default channel last-n value.
  55. * @type {number}
  56. */
  57. this.channelLastN
  58. = ('number' === typeof(config.channelLastN)) ? config.channelLastN : -1;
  59. // media types of the conference
  60. if (config.openSctp)
  61. this.media = ['audio', 'video', 'data'];
  62. else
  63. this.media = ['audio', 'video'];
  64. this.connection.jingle.sessions[this.sid] = this;
  65. this.bundledTransports = {};
  66. this.mychannel = [];
  67. this.channels = [];
  68. this.remotessrc = {};
  69. // container for candidates from the focus
  70. // gathered before confid is known
  71. this.drip_container = [];
  72. // silly wait flag
  73. this.wait = true;
  74. this.recordingEnabled = false;
  75. // stores information about the endpoints (i.e. display names) to
  76. // be sent to the videobridge.
  77. this.endpointsInfo = null;
  78. }
  79. // creates a conferences with an initial set of peers
  80. ColibriFocus.prototype.makeConference = function (peers) {
  81. var self = this;
  82. if (this.confid !== null) {
  83. console.error('makeConference called twice? Ignoring...');
  84. // FIXME: just invite peers?
  85. return;
  86. }
  87. this.confid = 0; // !null
  88. this.peers = [];
  89. peers.forEach(function (peer) {
  90. self.peers.push(peer);
  91. self.channels.push([]);
  92. });
  93. this.peerconnection
  94. = new TraceablePeerConnection(
  95. this.connection.jingle.ice_config,
  96. this.connection.jingle.pc_constraints );
  97. if(this.connection.jingle.localAudio) {
  98. this.peerconnection.addStream(this.connection.jingle.localAudio);
  99. }
  100. if(this.connection.jingle.localVideo) {
  101. this.peerconnection.addStream(this.connection.jingle.localVideo);
  102. }
  103. this.peerconnection.oniceconnectionstatechange = function (event) {
  104. console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
  105. /*
  106. if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
  107. console.log('adding new remote SSRCs from iceconnectionstatechange');
  108. window.setTimeout(function() { self.modifySources(); }, 1000);
  109. }
  110. */
  111. $(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
  112. };
  113. this.peerconnection.onsignalingstatechange = function (event) {
  114. console.warn(self.peerconnection.signalingState);
  115. /*
  116. if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
  117. console.log('adding new remote SSRCs from signalingstatechange');
  118. window.setTimeout(function() { self.modifySources(); }, 1000);
  119. }
  120. */
  121. };
  122. this.peerconnection.onaddstream = function (event) {
  123. // search the jid associated with this stream
  124. Object.keys(self.remotessrc).forEach(function (jid) {
  125. if (self.remotessrc[jid].join('\r\n').indexOf('mslabel:' + event.stream.id) != -1) {
  126. event.peerjid = jid;
  127. }
  128. });
  129. $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
  130. };
  131. this.peerconnection.onicecandidate = function (event) {
  132. //console.log('focus onicecandidate', self.confid, new Date().getTime(), event.candidate);
  133. if (!event.candidate) {
  134. console.log('end of candidates');
  135. return;
  136. }
  137. self.sendIceCandidate(event.candidate);
  138. };
  139. this._makeConference();
  140. /*
  141. this.peerconnection.createOffer(
  142. function (offer) {
  143. self.peerconnection.setLocalDescription(
  144. offer,
  145. function () {
  146. // success
  147. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  148. // FIXME: could call _makeConference here and trickle candidates later
  149. self._makeConference();
  150. },
  151. function (error) {
  152. console.log('setLocalDescription failed', error);
  153. }
  154. );
  155. },
  156. function (error) {
  157. console.warn(error);
  158. }
  159. );
  160. */
  161. };
  162. // Sends a COLIBRI message which enables or disables (according to 'state') the
  163. // recording on the bridge. Waits for the result IQ and calls 'callback' with
  164. // the new recording state, according to the IQ.
  165. ColibriFocus.prototype.setRecording = function(state, token, callback) {
  166. var self = this;
  167. var elem = $iq({to: this.bridgejid, type: 'set'});
  168. elem.c('conference', {
  169. xmlns: 'http://jitsi.org/protocol/colibri',
  170. id: this.confid
  171. });
  172. elem.c('recording', {state: state, token: token});
  173. elem.up();
  174. this.connection.sendIQ(elem,
  175. function (result) {
  176. console.log('Set recording "', state, '". Result:', result);
  177. var recordingElem = $(result).find('>conference>recording');
  178. var newState = ('true' === recordingElem.attr('state'));
  179. self.recordingEnabled = newState;
  180. callback(newState);
  181. },
  182. function (error) {
  183. console.warn(error);
  184. }
  185. );
  186. };
  187. /*
  188. * Updates the display name for an endpoint with a specific jid.
  189. * jid: the jid associated with the endpoint.
  190. * displayName: the new display name for the endpoint.
  191. */
  192. ColibriFocus.prototype.setEndpointDisplayName = function(jid, displayName) {
  193. var endpointId = jid.substr(1 + jid.lastIndexOf('/'));
  194. var update = false;
  195. if (this.endpointsInfo === null) {
  196. this.endpointsInfo = {};
  197. }
  198. var endpointInfo = this.endpointsInfo[endpointId];
  199. if ('undefined' === typeof endpointInfo) {
  200. endpointInfo = this.endpointsInfo[endpointId] = {};
  201. }
  202. if (endpointInfo['displayname'] !== displayName) {
  203. endpointInfo['displayname'] = displayName;
  204. update = true;
  205. }
  206. if (update) {
  207. this.updateEndpoints();
  208. }
  209. };
  210. /*
  211. * Sends a colibri message to the bridge that contains the
  212. * current endpoints and their display names.
  213. */
  214. ColibriFocus.prototype.updateEndpoints = function() {
  215. if (this.confid === null
  216. || this.endpointsInfo === null) {
  217. return;
  218. }
  219. if (this.confid === 0) {
  220. // the colibri conference is currently initiating
  221. var self = this;
  222. window.setTimeout(function() { self.updateEndpoints()}, 1000);
  223. return;
  224. }
  225. var elem = $iq({to: this.bridgejid, type: 'set'});
  226. elem.c('conference', {
  227. xmlns: 'http://jitsi.org/protocol/colibri',
  228. id: this.confid
  229. });
  230. for (var id in this.endpointsInfo) {
  231. elem.c('endpoint');
  232. elem.attrs({ id: id,
  233. displayname: this.endpointsInfo[id]['displayname']
  234. });
  235. elem.up();
  236. }
  237. //elem.up(); //conference
  238. this.connection.sendIQ(
  239. elem,
  240. function (result) {},
  241. function (error) { console.warn(error); }
  242. );
  243. };
  244. ColibriFocus.prototype._makeConference = function () {
  245. var self = this;
  246. var elem = $iq({ to: this.bridgejid, type: 'get' });
  247. elem.c('conference', { xmlns: 'http://jitsi.org/protocol/colibri' });
  248. this.media.forEach(function (name) {
  249. var elemName;
  250. var elemAttrs = { initiator: 'true', expire: self.channelExpire };
  251. if ('data' === name)
  252. {
  253. elemName = 'sctpconnection';
  254. elemAttrs['port'] = 5000;
  255. }
  256. else
  257. {
  258. elemName = 'channel';
  259. if (('video' === name) && (self.channelLastN >= 0))
  260. elemAttrs['last-n'] = self.channelLastN;
  261. }
  262. elem.c('content', { name: name });
  263. elem.c(elemName, elemAttrs);
  264. elem.attrs({ endpoint: self.myMucResource });
  265. if (config.useBundle) {
  266. elem.attrs({ 'channel-bundle-id': self.myMucResource });
  267. }
  268. elem.up();// end of channel/sctpconnection
  269. for (var j = 0; j < self.peers.length; j++) {
  270. var peer = self.peers[j];
  271. var peerEndpoint = peer.substr(1 + peer.lastIndexOf('/'));
  272. elem.c(elemName, elemAttrs);
  273. elem.attrs({ endpoint: peerEndpoint });
  274. if (config.useBundle) {
  275. elem.attrs({ 'channel-bundle-id': peerEndpoint });
  276. }
  277. elem.up(); // end of channel/sctpconnection
  278. }
  279. elem.up(); // end of content
  280. });
  281. if (this.endpointsInfo !== null) {
  282. for (var id in this.endpointsInfo) {
  283. elem.c('endpoint');
  284. elem.attrs({ id: id,
  285. displayname: this.endpointsInfo[id]['displayname']
  286. });
  287. elem.up();
  288. }
  289. }
  290. /*
  291. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  292. localSDP.media.forEach(function (media, channel) {
  293. var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
  294. elem.c('content', {name: name});
  295. elem.c('channel', {initiator: 'false', expire: self.channelExpire});
  296. // FIXME: should reuse code from .toJingle
  297. var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
  298. for (var j = 0; j < mline.fmt.length; j++) {
  299. var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
  300. elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
  301. elem.up();
  302. }
  303. localSDP.TransportToJingle(channel, elem);
  304. elem.up(); // end of channel
  305. for (j = 0; j < self.peers.length; j++) {
  306. elem.c('channel', {initiator: 'true', expire: self.channelExpire }).up();
  307. }
  308. elem.up(); // end of content
  309. });
  310. */
  311. this.connection.sendIQ(elem,
  312. function (result) {
  313. self.createdConference(result);
  314. },
  315. function (error) {
  316. console.warn(error);
  317. }
  318. );
  319. };
  320. // callback when a colibri conference was created
  321. ColibriFocus.prototype.createdConference = function (result) {
  322. console.log('created a conference on the bridge');
  323. var self = this;
  324. var tmp;
  325. this.confid = $(result).find('>conference').attr('id');
  326. var remotecontents = $(result).find('>conference>content').get();
  327. var numparticipants = 0;
  328. for (var i = 0; i < remotecontents.length; i++)
  329. {
  330. var contentName = $(remotecontents[i]).attr('name');
  331. var channelName
  332. = contentName !== 'data' ? '>channel' : '>sctpconnection';
  333. tmp = $(remotecontents[i]).find(channelName).get();
  334. this.mychannel.push($(tmp.shift()));
  335. numparticipants = tmp.length;
  336. for (j = 0; j < tmp.length; j++) {
  337. if (this.channels[j] === undefined) {
  338. this.channels[j] = [];
  339. }
  340. this.channels[j].push(tmp[j]);
  341. }
  342. }
  343. // save the 'transport' elements from 'channel-bundle'-s
  344. var channelBundles = $(result).find('>conference>channel-bundle');
  345. for (var i = 0; i < channelBundles.length; i++)
  346. {
  347. var endpointId = $(channelBundles[i]).attr('id');
  348. this.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  349. }
  350. console.log('remote channels', this.channels);
  351. // Notify that the focus has created the conference on the bridge
  352. $(document).trigger('conferenceCreated.jingle', [self]);
  353. var bridgeSDP = new SDP(
  354. 'v=0\r\n' +
  355. 'o=- 5151055458874951233 2 IN IP4 127.0.0.1\r\n' +
  356. 's=-\r\n' +
  357. 't=0 0\r\n' +
  358. /* Audio */
  359. (config.useBundle
  360. ? ('a=group:BUNDLE audio video' +
  361. (config.openSctp ? ' data' : '') +
  362. '\r\n')
  363. : '') +
  364. 'm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\n' +
  365. 'c=IN IP4 0.0.0.0\r\n' +
  366. 'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
  367. 'a=mid:audio\r\n' +
  368. 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' +
  369. 'a=sendrecv\r\n' +
  370. 'a=rtpmap:111 opus/48000/2\r\n' +
  371. 'a=fmtp:111 minptime=10\r\n' +
  372. 'a=rtpmap:103 ISAC/16000\r\n' +
  373. 'a=rtpmap:104 ISAC/32000\r\n' +
  374. 'a=rtpmap:0 PCMU/8000\r\n' +
  375. 'a=rtpmap:8 PCMA/8000\r\n' +
  376. 'a=rtpmap:106 CN/32000\r\n' +
  377. 'a=rtpmap:105 CN/16000\r\n' +
  378. 'a=rtpmap:13 CN/8000\r\n' +
  379. 'a=rtpmap:126 telephone-event/8000\r\n' +
  380. 'a=maxptime:60\r\n' +
  381. (config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
  382. /* Video */
  383. 'm=video 1 RTP/SAVPF 100 116 117\r\n' +
  384. 'c=IN IP4 0.0.0.0\r\n' +
  385. 'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
  386. 'a=mid:video\r\n' +
  387. 'a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n' +
  388. 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
  389. 'a=sendrecv\r\n' +
  390. 'a=rtpmap:100 VP8/90000\r\n' +
  391. 'a=rtcp-fb:100 ccm fir\r\n' +
  392. 'a=rtcp-fb:100 nack\r\n' +
  393. 'a=rtcp-fb:100 goog-remb\r\n' +
  394. 'a=rtpmap:116 red/90000\r\n' +
  395. 'a=rtpmap:117 ulpfec/90000\r\n' +
  396. (config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
  397. /* Data SCTP */
  398. (config.openSctp ?
  399. 'm=application 1 DTLS/SCTP 5000\r\n' +
  400. 'c=IN IP4 0.0.0.0\r\n' +
  401. 'a=sctpmap:5000 webrtc-datachannel\r\n' +
  402. 'a=mid:data\r\n'
  403. : '')
  404. );
  405. bridgeSDP.media.length = this.mychannel.length;
  406. var channel;
  407. /*
  408. for (channel = 0; channel < bridgeSDP.media.length; channel++) {
  409. bridgeSDP.media[channel] = '';
  410. // unchanged lines
  411. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'm=') + '\r\n';
  412. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'c=') + '\r\n';
  413. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:')) {
  414. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:') + '\r\n';
  415. }
  416. if (SDPUtil.find_line(localSDP.media[channel], 'a=mid:')) {
  417. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=mid:') + '\r\n';
  418. }
  419. if (SDPUtil.find_line(localSDP.media[channel], 'a=sendrecv')) {
  420. bridgeSDP.media[channel] += 'a=sendrecv\r\n';
  421. }
  422. if (SDPUtil.find_line(localSDP.media[channel], 'a=extmap:')) {
  423. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=extmap:').join('\r\n') + '\r\n';
  424. }
  425. // FIXME: should look at m-line and group the ids together
  426. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtpmap:')) {
  427. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtpmap:').join('\r\n') + '\r\n';
  428. }
  429. if (SDPUtil.find_line(localSDP.media[channel], 'a=fmtp:')) {
  430. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=fmtp:').join('\r\n') + '\r\n';
  431. }
  432. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp-fb:')) {
  433. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtcp-fb:').join('\r\n') + '\r\n';
  434. }
  435. // FIXME: changed lines -- a=sendrecv direction, a=setup direction
  436. }
  437. */
  438. for (channel = 0; channel < bridgeSDP.media.length; channel++) {
  439. // get the mixed ssrc
  440. tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  441. // FIXME: check rtp-level-relay-type
  442. var name = bridgeSDP.media[channel].split(" ")[0].substr(2); // 'm=audio ...'
  443. if (name === 'audio' || name === 'video') {
  444. // make chrome happy... '3735928559' == 0xDEADBEEF
  445. var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
  446. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
  447. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
  448. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
  449. bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
  450. }
  451. // FIXME: should take code from .fromJingle
  452. var channelBundleId = $(this.mychannel[channel]).attr('channel-bundle-id');
  453. if (typeof channelBundleId != 'undefined') {
  454. tmp = this.bundledTransports[channelBundleId];
  455. } else {
  456. tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  457. }
  458. if (tmp.length) {
  459. bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  460. bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  461. tmp.find('>candidate').each(function () {
  462. bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
  463. });
  464. tmp = tmp.find('>fingerprint');
  465. if (tmp.length) {
  466. bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  467. bridgeSDP.media[channel] += 'a=setup:actpass\r\n'; // offer so always actpass
  468. }
  469. }
  470. }
  471. bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
  472. this.peerconnection.setRemoteDescription(
  473. new RTCSessionDescription({type: 'offer', sdp: bridgeSDP.raw}),
  474. function () {
  475. console.log('setRemoteDescription success');
  476. self.peerconnection.createAnswer(
  477. function (answer) {
  478. self.peerconnection.setLocalDescription(answer,
  479. function () {
  480. console.log('setLocalDescription succeeded.');
  481. // make sure our presence is updated
  482. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  483. var elem = $iq({to: self.bridgejid, type: 'get'});
  484. elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
  485. var localSDP = new SDP(self.peerconnection.localDescription.sdp);
  486. localSDP.media.forEach(function (media, channel) {
  487. var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
  488. elem.c('content', {name: name});
  489. var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
  490. if (name !== 'data')
  491. {
  492. elem.c('channel', {
  493. initiator: 'true',
  494. expire: self.channelExpire,
  495. id: self.mychannel[channel].attr('id'),
  496. endpoint: self.myMucResource
  497. });
  498. // FIXME: should reuse code from .toJingle
  499. for (var j = 0; j < mline.fmt.length; j++)
  500. {
  501. var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
  502. if (rtpmap)
  503. {
  504. elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
  505. elem.up();
  506. }
  507. }
  508. }
  509. else
  510. {
  511. var sctpmap = SDPUtil.find_line(media, 'a=sctpmap:' + mline.fmt[0]);
  512. var sctpPort = SDPUtil.parse_sctpmap(sctpmap)[0];
  513. elem.c("sctpconnection",
  514. {
  515. initiator: 'true',
  516. expire: self.channelExpire,
  517. id: self.mychannel[channel].attr('id'),
  518. endpoint: self.myMucResource,
  519. port: sctpPort
  520. }
  521. );
  522. }
  523. localSDP.TransportToJingle(channel, elem);
  524. elem.up(); // end of channel
  525. elem.up(); // end of content
  526. });
  527. self.connection.sendIQ(elem,
  528. function (result) {
  529. // ...
  530. },
  531. function (error) {
  532. console.error(
  533. "ERROR sending colibri message",
  534. error, elem);
  535. }
  536. );
  537. // now initiate sessions
  538. for (var i = 0; i < numparticipants; i++) {
  539. self.initiate(self.peers[i], true);
  540. }
  541. // Notify we've created the conference
  542. $(document).trigger(
  543. 'conferenceCreated.jingle', self);
  544. },
  545. function (error) {
  546. console.warn('setLocalDescription failed.', error);
  547. }
  548. );
  549. },
  550. function (error) {
  551. console.warn('createAnswer failed.', error);
  552. }
  553. );
  554. /*
  555. for (var i = 0; i < numparticipants; i++) {
  556. self.initiate(self.peers[i], true);
  557. }
  558. */
  559. },
  560. function (error) {
  561. console.log('setRemoteDescription failed.', error);
  562. }
  563. );
  564. };
  565. // send a session-initiate to a new participant
  566. ColibriFocus.prototype.initiate = function (peer, isInitiator) {
  567. var participant = this.peers.indexOf(peer);
  568. console.log('tell', peer, participant);
  569. var sdp;
  570. if (this.peerconnection !== null && this.peerconnection.signalingState == 'stable') {
  571. sdp = new SDP(this.peerconnection.remoteDescription.sdp);
  572. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  573. // throw away stuff we don't want
  574. // not needed with static offer
  575. if (!config.useBundle) {
  576. sdp.removeSessionLines('a=group:');
  577. }
  578. sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
  579. for (var i = 0; i < sdp.media.length; i++) {
  580. if (!config.useRtcpMux){
  581. sdp.removeMediaLines(i, 'a=rtcp-mux');
  582. }
  583. sdp.removeMediaLines(i, 'a=ssrc:');
  584. sdp.removeMediaLines(i, 'a=crypto:');
  585. sdp.removeMediaLines(i, 'a=candidate:');
  586. sdp.removeMediaLines(i, 'a=ice-options:google-ice');
  587. sdp.removeMediaLines(i, 'a=ice-ufrag:');
  588. sdp.removeMediaLines(i, 'a=ice-pwd:');
  589. sdp.removeMediaLines(i, 'a=fingerprint:');
  590. sdp.removeMediaLines(i, 'a=setup:');
  591. if (1) { //i > 0) { // not for audio FIXME: does not work as intended
  592. // re-add all remote a=ssrcs
  593. for (var jid in this.remotessrc) {
  594. if (jid == peer || !this.remotessrc[jid][i])
  595. continue;
  596. sdp.media[i] += this.remotessrc[jid][i];
  597. }
  598. // and local a=ssrc lines
  599. sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc').join('\r\n') + '\r\n';
  600. }
  601. }
  602. sdp.raw = sdp.session + sdp.media.join('');
  603. } else {
  604. console.error('can not initiate a new session without a stable peerconnection');
  605. return;
  606. }
  607. // add stuff we got from the bridge
  608. for (var j = 0; j < sdp.media.length; j++) {
  609. var chan = $(this.channels[participant][j]);
  610. console.log('channel id', chan.attr('id'));
  611. tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  612. var name = sdp.media[j].split(" ")[0].substr(2); // 'm=audio ...'
  613. if (name === 'audio' || name === 'video') {
  614. // make chrome happy... '3735928559' == 0xDEADBEEF
  615. var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
  616. sdp.media[j] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
  617. sdp.media[j] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
  618. sdp.media[j] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
  619. sdp.media[j] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
  620. }
  621. // In the case of bundle, we add each candidate to all m= lines/jingle contents,
  622. // just as chrome does
  623. if (config.useBundle){
  624. tmp = this.bundledTransports[chan.attr('channel-bundle-id')];
  625. } else {
  626. tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  627. }
  628. if (tmp.length) {
  629. if (tmp.attr('ufrag'))
  630. sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  631. if (tmp.attr('pwd'))
  632. sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  633. // and the candidates...
  634. tmp.find('>candidate').each(function () {
  635. sdp.media[j] += SDPUtil.candidateFromJingle(this);
  636. });
  637. tmp = tmp.find('>fingerprint');
  638. if (tmp.length) {
  639. sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  640. /*
  641. if (tmp.attr('direction')) {
  642. sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
  643. }
  644. */
  645. sdp.media[j] += 'a=setup:actpass\r\n';
  646. }
  647. }
  648. }
  649. // make a new colibri session and configure it
  650. // FIXME: is it correct to use this.connection.jid when used in a MUC?
  651. var sess = new ColibriSession(this.connection.jid,
  652. Math.random().toString(36).substr(2, 12), // random string
  653. this.connection);
  654. sess.initiate(peer);
  655. sess.colibri = this;
  656. // We do not announce our audio per conference peer, so only video is set here
  657. sess.localVideo = this.connection.jingle.localVideo;
  658. sess.media_constraints = this.connection.jingle.media_constraints;
  659. sess.pc_constraints = this.connection.jingle.pc_constraints;
  660. sess.ice_config = this.connection.jingle.ice_config;
  661. this.connection.jingle.sessions[sess.sid] = sess;
  662. this.connection.jingle.jid2session[sess.peerjid] = sess;
  663. // send a session-initiate
  664. var init = $iq({to: peer, type: 'set'})
  665. .c('jingle',
  666. {xmlns: 'urn:xmpp:jingle:1',
  667. action: 'session-initiate',
  668. initiator: sess.me,
  669. sid: sess.sid
  670. }
  671. );
  672. sdp.toJingle(init, 'initiator');
  673. this.connection.sendIQ(init,
  674. function (res) {
  675. console.log('got result');
  676. },
  677. function (err) {
  678. console.log('got error');
  679. }
  680. );
  681. };
  682. // pull in a new participant into the conference
  683. ColibriFocus.prototype.addNewParticipant = function (peer) {
  684. var self = this;
  685. if (this.confid === 0 || !this.peerconnection.localDescription)
  686. {
  687. // bad state
  688. if (this.confid === 0)
  689. {
  690. console.error('confid does not exist yet, postponing', peer);
  691. }
  692. else
  693. {
  694. console.error('local description not ready yet, postponing', peer);
  695. }
  696. window.setTimeout(function () { self.addNewParticipant(peer); }, 250);
  697. return;
  698. }
  699. var index = this.channels.length;
  700. this.channels.push([]);
  701. this.peers.push(peer);
  702. var elem = $iq({to: this.bridgejid, type: 'get'});
  703. elem.c(
  704. 'conference',
  705. { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
  706. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  707. localSDP.media.forEach(function (media, channel) {
  708. var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
  709. var elemName;
  710. var endpointId = peer.substr(1 + peer.lastIndexOf('/'));
  711. var elemAttrs
  712. = {
  713. initiator: 'true',
  714. expire: self.channelExpire,
  715. endpoint: endpointId
  716. };
  717. if (config.useBundle) {
  718. elemAttrs['channel-bundle-id'] = endpointId;
  719. }
  720. if ('data' == name)
  721. {
  722. elemName = 'sctpconnection';
  723. elemAttrs['port'] = 5000;
  724. }
  725. else
  726. {
  727. elemName = 'channel';
  728. if (('video' === name) && (self.channelLastN >= 0))
  729. elemAttrs['last-n'] = self.channelLastN;
  730. }
  731. elem.c('content', { name: name });
  732. elem.c(elemName, elemAttrs);
  733. elem.up(); // end of channel/sctpconnection
  734. elem.up(); // end of content
  735. });
  736. this.connection.sendIQ(elem,
  737. function (result) {
  738. var contents = $(result).find('>conference>content').get();
  739. var i;
  740. for (i = 0; i < contents.length; i++) {
  741. var channelXml = $(contents[i]).find('>channel');
  742. if (channelXml.length)
  743. {
  744. tmp = channelXml.get();
  745. }
  746. else
  747. {
  748. tmp = $(contents[i]).find('>sctpconnection').get();
  749. }
  750. self.channels[index][i] = tmp[0];
  751. }
  752. var channelBundles = $(result).find('>conference>channel-bundle');
  753. for (i = 0; i < channelBundles.length; i++)
  754. {
  755. var endpointId = $(channelBundles[i]).attr('id');
  756. self.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  757. }
  758. self.initiate(peer, true);
  759. },
  760. function (error) {
  761. console.warn(error);
  762. }
  763. );
  764. };
  765. // update the channel description (payload-types + dtls fp) for a participant
  766. ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
  767. console.log('change allocation for', this.confid);
  768. var self = this;
  769. var change = $iq({to: this.bridgejid, type: 'set'});
  770. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  771. for (channel = 0; channel < this.channels[participant].length; channel++)
  772. {
  773. if (!remoteSDP.media[channel])
  774. continue;
  775. var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
  776. change.c('content', {name: name});
  777. if (name !== 'data')
  778. {
  779. change.c('channel', {
  780. id: $(this.channels[participant][channel]).attr('id'),
  781. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  782. expire: self.channelExpire
  783. });
  784. var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
  785. rtpmap.forEach(function (val) {
  786. // TODO: too much copy-paste
  787. var rtpmap = SDPUtil.parse_rtpmap(val);
  788. change.c('payload-type', rtpmap);
  789. //
  790. // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
  791. /*
  792. if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
  793. tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
  794. for (var k = 0; k < tmp.length; k++) {
  795. change.c('parameter', tmp[k]).up();
  796. }
  797. }
  798. */
  799. change.up();
  800. });
  801. }
  802. else
  803. {
  804. var sctpmap = SDPUtil.find_line(remoteSDP.media[channel], 'a=sctpmap:');
  805. change.c('sctpconnection', {
  806. id: $(this.channels[participant][channel]).attr('id'),
  807. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  808. expire: self.channelExpire,
  809. port: SDPUtil.parse_sctpmap(sctpmap)[0]
  810. });
  811. }
  812. // now add transport
  813. remoteSDP.TransportToJingle(channel, change);
  814. change.up(); // end of channel/sctpconnection
  815. change.up(); // end of content
  816. }
  817. this.connection.sendIQ(change,
  818. function (res) {
  819. console.log('got result');
  820. },
  821. function (err) {
  822. console.log('got error');
  823. }
  824. );
  825. };
  826. // tell everyone about a new participants a=ssrc lines (isadd is true)
  827. // or a leaving participants a=ssrc lines
  828. ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd) {
  829. var self = this;
  830. this.peers.forEach(function (peerjid) {
  831. if (peerjid == fromJid) return;
  832. console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', fromJid);
  833. if (!self.remotessrc[peerjid]) {
  834. // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
  835. // possibly, this.remoteSSRC[session.peerjid] does not exist yet
  836. console.warn('do we really want to bother', peerjid, 'with updates yet?');
  837. }
  838. var peersess = self.connection.jingle.jid2session[peerjid];
  839. if(!peersess){
  840. console.warn('no session with peer: '+peerjid+' yet...');
  841. return;
  842. }
  843. self.sendSSRCUpdateIq(sdpMediaSsrcs, peersess.sid, peersess.initiator, peerjid, isadd);
  844. });
  845. };
  846. /**
  847. * Overrides SessionBase.addSource.
  848. *
  849. * @param elem proprietary 'add source' Jingle request(XML node).
  850. * @param fromJid JID of the participant to whom new ssrcs belong.
  851. */
  852. ColibriFocus.prototype.addSource = function (elem, fromJid) {
  853. var self = this;
  854. // FIXME: dirty waiting
  855. if (!this.peerconnection.localDescription)
  856. {
  857. console.warn("addSource - localDescription not ready yet")
  858. setTimeout(function() { self.addSource(elem, fromJid); }, 200);
  859. return;
  860. }
  861. this.peerconnection.addSource(elem);
  862. var peerSsrc = this.remotessrc[fromJid];
  863. //console.log("On ADD", self.addssrc, peerSsrc);
  864. this.peerconnection.addssrc.forEach(function(val, idx){
  865. if(!peerSsrc[idx]){
  866. // add ssrc
  867. peerSsrc[idx] = val;
  868. } else {
  869. if(peerSsrc[idx].indexOf(val) == -1){
  870. peerSsrc[idx] = peerSsrc[idx]+val;
  871. }
  872. }
  873. });
  874. var oldRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
  875. this.modifySources(function(){
  876. // Notify other participants about added ssrc
  877. var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
  878. var newSSRCs = oldRemoteSdp.getNewMedia(remoteSDP);
  879. self.sendSSRCUpdate(newSSRCs, fromJid, true);
  880. });
  881. };
  882. /**
  883. * Overrides SessionBase.removeSource.
  884. *
  885. * @param elem proprietary 'remove source' Jingle request(XML node).
  886. * @param fromJid JID of the participant to whom removed ssrcs belong.
  887. */
  888. ColibriFocus.prototype.removeSource = function (elem, fromJid) {
  889. var self = this;
  890. // FIXME: dirty waiting
  891. if (!self.peerconnection.localDescription)
  892. {
  893. console.warn("removeSource - localDescription not ready yet");
  894. setTimeout(function() { self.removeSource(elem, fromJid); }, 200);
  895. return;
  896. }
  897. this.peerconnection.removeSource(elem);
  898. var peerSsrc = this.remotessrc[fromJid];
  899. //console.log("On REMOVE", self.removessrc, peerSsrc);
  900. this.peerconnection.removessrc.forEach(function(val, idx){
  901. if(peerSsrc[idx]){
  902. // Remove ssrc
  903. peerSsrc[idx] = peerSsrc[idx].replace(val, '');
  904. }
  905. });
  906. var oldSDP = new SDP(self.peerconnection.remoteDescription.sdp);
  907. this.modifySources(function(){
  908. // Notify other participants about removed ssrc
  909. var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
  910. var removedSSRCs = remoteSDP.getNewMedia(oldSDP);
  911. self.sendSSRCUpdate(removedSSRCs, fromJid, false);
  912. });
  913. };
  914. ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
  915. var participant = this.peers.indexOf(session.peerjid);
  916. console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
  917. var remoteSDP = new SDP('');
  918. var channel;
  919. remoteSDP.fromJingle(elem);
  920. // ACT 1: change allocation on bridge
  921. this.updateChannel(remoteSDP, participant);
  922. // ACT 2: tell anyone else about the new SSRCs
  923. this.sendSSRCUpdate(remoteSDP.getMediaSsrcMap(), session.peerjid, true);
  924. // ACT 3: note the SSRCs
  925. this.remotessrc[session.peerjid] = [];
  926. for (channel = 0; channel < this.channels[participant].length; channel++) {
  927. //if (channel == 0) continue; FIXME: does not work as intended
  928. if (!remoteSDP.media[channel])
  929. continue;
  930. if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
  931. {
  932. this.remotessrc[session.peerjid][channel] =
  933. SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
  934. .join('\r\n') + '\r\n';
  935. }
  936. }
  937. // ACT 4: add new a=ssrc lines to local remotedescription
  938. for (channel = 0; channel < this.channels[participant].length; channel++) {
  939. //if (channel == 0) continue; FIXME: does not work as intended
  940. if (!remoteSDP.media[channel])
  941. continue;
  942. if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
  943. this.peerconnection.enqueueAddSsrc(
  944. channel,
  945. SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'
  946. );
  947. }
  948. }
  949. this.modifySources();
  950. };
  951. // relay ice candidates to bridge using trickle
  952. ColibriFocus.prototype.addIceCandidate = function (session, elem) {
  953. var self = this;
  954. var participant = this.peers.indexOf(session.peerjid);
  955. //console.log('change transport allocation for', this.confid, session.peerjid, participant);
  956. var change = $iq({to: this.bridgejid, type: 'set'});
  957. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  958. $(elem).each(function () {
  959. var name = $(this).attr('name');
  960. // If we are using bundle, audio/video/data channel will have the same candidates, so only send them for
  961. // the audio channel.
  962. if (config.useBundle && name !== 'audio') {
  963. return;
  964. }
  965. var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
  966. if (name != 'audio' && name != 'video')
  967. channel = 2; // name == 'data'
  968. change.c('content', {name: name});
  969. if (name !== 'data')
  970. {
  971. change.c('channel', {
  972. id: $(self.channels[participant][channel]).attr('id'),
  973. endpoint: $(self.channels[participant][channel]).attr('endpoint'),
  974. expire: self.channelExpire
  975. });
  976. }
  977. else
  978. {
  979. change.c('sctpconnection', {
  980. id: $(self.channels[participant][channel]).attr('id'),
  981. endpoint: $(self.channels[participant][channel]).attr('endpoint'),
  982. expire: self.channelExpire
  983. });
  984. }
  985. $(this).find('>transport').each(function () {
  986. change.c('transport', {
  987. ufrag: $(this).attr('ufrag'),
  988. pwd: $(this).attr('pwd'),
  989. xmlns: $(this).attr('xmlns')
  990. });
  991. if (config.useRtcpMux
  992. && 'channel' === change.node.parentNode.nodeName) {
  993. change.c('rtcp-mux').up();
  994. }
  995. $(this).find('>candidate').each(function () {
  996. /* not yet
  997. if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
  998. // chrome generates TCP candidates with port 0
  999. return;
  1000. }
  1001. */
  1002. var line = SDPUtil.candidateFromJingle(this);
  1003. change.c('candidate', SDPUtil.candidateToJingle(line)).up();
  1004. });
  1005. change.up(); // end of transport
  1006. });
  1007. change.up(); // end of channel/sctpconnection
  1008. change.up(); // end of content
  1009. });
  1010. // FIXME: need to check if there is at least one candidate when filtering TCP ones
  1011. this.connection.sendIQ(change,
  1012. function (res) {
  1013. console.log('got result');
  1014. },
  1015. function (err) {
  1016. console.error('got error', err);
  1017. }
  1018. );
  1019. };
  1020. // send our own candidate to the bridge
  1021. ColibriFocus.prototype.sendIceCandidate = function (candidate) {
  1022. var self = this;
  1023. //console.log('candidate', candidate);
  1024. if (!candidate) {
  1025. console.log('end of candidates');
  1026. return;
  1027. }
  1028. if (this.drip_container.length === 0) {
  1029. // start 20ms callout
  1030. window.setTimeout(
  1031. function () {
  1032. if (self.drip_container.length === 0) return;
  1033. self.sendIceCandidates(self.drip_container);
  1034. self.drip_container = [];
  1035. },
  1036. 20);
  1037. }
  1038. this.drip_container.push(candidate);
  1039. };
  1040. // sort and send multiple candidates
  1041. ColibriFocus.prototype.sendIceCandidates = function (candidates) {
  1042. var self = this;
  1043. var mycands = $iq({to: this.bridgejid, type: 'set'});
  1044. mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  1045. // FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
  1046. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  1047. for (var mid = 0; mid < localSDP.media.length; mid++)
  1048. {
  1049. var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
  1050. if (cands.length > 0)
  1051. {
  1052. var name = cands[0].sdpMid;
  1053. mycands.c('content', {name: name });
  1054. if (name !== 'data')
  1055. {
  1056. mycands.c('channel', {
  1057. id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
  1058. endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
  1059. expire: self.channelExpire
  1060. });
  1061. }
  1062. else
  1063. {
  1064. mycands.c('sctpconnection', {
  1065. id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
  1066. endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
  1067. port: $(this.mychannel[cands[0].sdpMLineIndex]).attr('port'),
  1068. expire: self.channelExpire
  1069. });
  1070. }
  1071. mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
  1072. if (config.useRtcpMux && name !== 'data') {
  1073. mycands.c('rtcp-mux').up();
  1074. }
  1075. for (var i = 0; i < cands.length; i++) {
  1076. mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
  1077. }
  1078. mycands.up(); // transport
  1079. mycands.up(); // channel / sctpconnection
  1080. mycands.up(); // content
  1081. }
  1082. }
  1083. console.log('send cands', candidates);
  1084. this.connection.sendIQ(mycands,
  1085. function (res) {
  1086. console.log('got result');
  1087. },
  1088. function (err) {
  1089. console.error('got error', err);
  1090. }
  1091. );
  1092. };
  1093. ColibriFocus.prototype.terminate = function (session, reason) {
  1094. console.log('remote session terminated from', session.peerjid);
  1095. var participant = this.peers.indexOf(session.peerjid);
  1096. if (!this.remotessrc[session.peerjid] || participant == -1) {
  1097. return;
  1098. }
  1099. var ssrcs = this.remotessrc[session.peerjid];
  1100. for (var i = 0; i < ssrcs.length; i++) {
  1101. this.peerconnection.enqueueRemoveSsrc(i, ssrcs[i]);
  1102. }
  1103. // remove from this.peers
  1104. this.peers.splice(participant, 1);
  1105. // expire channel on bridge
  1106. var change = $iq({to: this.bridgejid, type: 'set'});
  1107. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  1108. for (var channel = 0; channel < this.channels[participant].length; channel++) {
  1109. var name = channel === 0 ? 'audio' : 'video';
  1110. if (channel == 2)
  1111. name = 'data';
  1112. change.c('content', {name: name});
  1113. if (name !== 'data')
  1114. {
  1115. change.c('channel', {
  1116. id: $(this.channels[participant][channel]).attr('id'),
  1117. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  1118. expire: '0'
  1119. });
  1120. }
  1121. else
  1122. {
  1123. change.c('sctpconnection', {
  1124. id: $(this.channels[participant][channel]).attr('id'),
  1125. endpoint: $(this.channels[participant][channel]).attr('endpoint'),
  1126. expire: '0'
  1127. });
  1128. }
  1129. change.up(); // end of channel/sctpconnection
  1130. change.up(); // end of content
  1131. }
  1132. this.connection.sendIQ(change,
  1133. function (res) {
  1134. console.log('got result');
  1135. },
  1136. function (err) {
  1137. console.error('got error', err);
  1138. }
  1139. );
  1140. // and remove from channels
  1141. this.channels.splice(participant, 1);
  1142. // tell everyone about the ssrcs to be removed
  1143. var sdp = new SDP('');
  1144. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  1145. var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
  1146. for (var j = 0; j < ssrcs.length; j++) {
  1147. sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
  1148. sdp.media[j] += ssrcs[j];
  1149. this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]);
  1150. }
  1151. this.sendSSRCUpdate(sdp.getMediaSsrcMap(), session.peerjid, false);
  1152. delete this.remotessrc[session.peerjid];
  1153. this.modifySources();
  1154. };
  1155. ColibriFocus.prototype.sendTerminate = function (session, reason, text) {
  1156. var term = $iq({to: session.peerjid, type: 'set'})
  1157. .c('jingle',
  1158. {xmlns: 'urn:xmpp:jingle:1',
  1159. action: 'session-terminate',
  1160. initiator: session.me,
  1161. sid: session.sid})
  1162. .c('reason')
  1163. .c(reason || 'success');
  1164. if (text) {
  1165. term.up().c('text').t(text);
  1166. }
  1167. this.connection.sendIQ(term,
  1168. function () {
  1169. if (!session)
  1170. return;
  1171. if (session.peerconnection) {
  1172. session.peerconnection.close();
  1173. session.peerconnection = null;
  1174. }
  1175. session.terminate();
  1176. var ack = {};
  1177. ack.source = 'terminate';
  1178. $(document).trigger('ack.jingle', [session.sid, ack]);
  1179. },
  1180. function (stanza) {
  1181. var error = ($(stanza).find('error').length) ? {
  1182. code: $(stanza).find('error').attr('code'),
  1183. reason: $(stanza).find('error :first')[0].tagName,
  1184. }:{};
  1185. $(document).trigger('ack.jingle', [self.sid, error]);
  1186. },
  1187. 10000);
  1188. if (this.statsinterval !== null) {
  1189. window.clearInterval(this.statsinterval);
  1190. this.statsinterval = null;
  1191. }
  1192. };
  1193. ColibriFocus.prototype.setRTCPTerminationStrategy = function (strategyFQN) {
  1194. var self = this;
  1195. var strategyIQ = $iq({to: this.bridgejid, type: 'set'});
  1196. strategyIQ.c('conference', {
  1197. xmlns: 'http://jitsi.org/protocol/colibri',
  1198. id: this.confid,
  1199. });
  1200. strategyIQ.c('rtcp-termination-strategy', {name: strategyFQN });
  1201. strategyIQ.c('content', {name: "video"});
  1202. strategyIQ.up(); // end of content
  1203. console.log('setting RTCP termination strategy', strategyFQN);
  1204. this.connection.sendIQ(strategyIQ,
  1205. function (res) {
  1206. console.log('got result');
  1207. },
  1208. function (err) {
  1209. console.error('got error', err);
  1210. }
  1211. );
  1212. };
  1213. /**
  1214. * Sets the default value of the channel last-n attribute in this conference and
  1215. * updates/patches the existing channels.
  1216. */
  1217. ColibriFocus.prototype.setChannelLastN = function (channelLastN) {
  1218. if (('number' === typeof(channelLastN))
  1219. && (this.channelLastN !== channelLastN))
  1220. {
  1221. this.channelLastN = channelLastN;
  1222. // Update/patch the existing channels.
  1223. var patch = $iq({ to: this.bridgejid, type: 'set' });
  1224. patch.c(
  1225. 'conference',
  1226. { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
  1227. patch.c('content', { name: 'video' });
  1228. patch.c(
  1229. 'channel',
  1230. {
  1231. id: $(this.mychannel[1 /* video */]).attr('id'),
  1232. 'last-n': this.channelLastN
  1233. });
  1234. patch.up(); // end of channel
  1235. for (var p = 0; p < this.channels.length; p++)
  1236. {
  1237. patch.c(
  1238. 'channel',
  1239. {
  1240. id: $(this.channels[p][1 /* video */]).attr('id'),
  1241. 'last-n': this.channelLastN
  1242. });
  1243. patch.up(); // end of channel
  1244. }
  1245. this.connection.sendIQ(
  1246. patch,
  1247. function (res) {
  1248. console.info('Set channel last-n succeeded:', res);
  1249. },
  1250. function (err) {
  1251. console.error('Set channel last-n failed:', err);
  1252. });
  1253. }
  1254. };