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 34KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  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. function ColibriFocus(connection, bridgejid) {
  35. this.connection = connection;
  36. this.bridgejid = bridgejid;
  37. this.peers = [];
  38. this.confid = null;
  39. this.peerconnection
  40. = new TraceablePeerConnection(
  41. this.connection.jingle.ice_config,
  42. this.connection.jingle.pc_constraints);
  43. // media types of the conference
  44. this.media = ['audio', 'video'];
  45. this.sid = Math.random().toString(36).substr(2, 12);
  46. this.connection.jingle.sessions[this.sid] = this;
  47. this.mychannel = [];
  48. this.channels = [];
  49. this.remotessrc = {};
  50. // container for candidates from the focus
  51. // gathered before confid is known
  52. this.drip_container = [];
  53. // silly wait flag
  54. this.wait = true;
  55. }
  56. // creates a conferences with an initial set of peers
  57. ColibriFocus.prototype.makeConference = function (peers) {
  58. var self = this;
  59. if (this.confid !== null) {
  60. console.error('makeConference called twice? Ignoring...');
  61. // FIXME: just invite peers?
  62. return;
  63. }
  64. this.confid = 0; // !null
  65. this.peers = [];
  66. peers.forEach(function (peer) {
  67. self.peers.push(peer);
  68. self.channels.push([]);
  69. });
  70. if(connection.jingle.localAudio) {
  71. this.peerconnection.addStream(connection.jingle.localAudio);
  72. }
  73. if(connection.jingle.localVideo) {
  74. this.peerconnection.addStream(connection.jingle.localVideo);
  75. }
  76. this.peerconnection.oniceconnectionstatechange = function (event) {
  77. console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
  78. /*
  79. if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
  80. console.log('adding new remote SSRCs from iceconnectionstatechange');
  81. window.setTimeout(function() { self.modifySources(); }, 1000);
  82. }
  83. */
  84. };
  85. this.peerconnection.onsignalingstatechange = function (event) {
  86. console.warn(self.peerconnection.signalingState);
  87. /*
  88. if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
  89. console.log('adding new remote SSRCs from signalingstatechange');
  90. window.setTimeout(function() { self.modifySources(); }, 1000);
  91. }
  92. */
  93. };
  94. this.peerconnection.onaddstream = function (event) {
  95. // search the jid associated with this stream
  96. Object.keys(self.remotessrc).forEach(function (jid) {
  97. if (self.remotessrc[jid].join('\r\n').indexOf('mslabel:' + event.stream.id) != -1) {
  98. event.peerjid = jid;
  99. }
  100. });
  101. $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
  102. };
  103. this.peerconnection.onicecandidate = function (event) {
  104. //console.log('focus onicecandidate', self.confid, new Date().getTime(), event.candidate);
  105. if (!event.candidate) {
  106. console.log('end of candidates');
  107. return;
  108. }
  109. self.sendIceCandidate(event.candidate);
  110. };
  111. this._makeConference();
  112. /*
  113. this.peerconnection.createOffer(
  114. function (offer) {
  115. self.peerconnection.setLocalDescription(
  116. offer,
  117. function () {
  118. // success
  119. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  120. // FIXME: could call _makeConference here and trickle candidates later
  121. self._makeConference();
  122. },
  123. function (error) {
  124. console.log('setLocalDescription failed', error);
  125. }
  126. );
  127. },
  128. function (error) {
  129. console.warn(error);
  130. }
  131. );
  132. */
  133. };
  134. ColibriFocus.prototype._makeConference = function () {
  135. var self = this;
  136. var elem = $iq({to: this.bridgejid, type: 'get'});
  137. elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
  138. this.media.forEach(function (name) {
  139. elem.c('content', {name: name});
  140. elem.c('channel', {initiator: 'true', expire: '15'}).up();
  141. for (var j = 0; j < self.peers.length; j++) {
  142. elem.c('channel', {initiator: 'true', expire:'15' }).up();
  143. }
  144. elem.up(); // end of content
  145. });
  146. /*
  147. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  148. localSDP.media.forEach(function (media, channel) {
  149. var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
  150. elem.c('content', {name: name});
  151. elem.c('channel', {initiator: 'false', expire: '15'});
  152. // FIXME: should reuse code from .toJingle
  153. var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
  154. for (var j = 0; j < mline.fmt.length; j++) {
  155. var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
  156. elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
  157. elem.up();
  158. }
  159. localSDP.TransportToJingle(channel, elem);
  160. elem.up(); // end of channel
  161. for (j = 0; j < self.peers.length; j++) {
  162. elem.c('channel', {initiator: 'true', expire:'15' }).up();
  163. }
  164. elem.up(); // end of content
  165. });
  166. */
  167. this.connection.sendIQ(elem,
  168. function (result) {
  169. self.createdConference(result);
  170. },
  171. function (error) {
  172. console.warn(error);
  173. }
  174. );
  175. };
  176. // callback when a conference was created
  177. ColibriFocus.prototype.createdConference = function (result) {
  178. console.log('created a conference on the bridge');
  179. var self = this;
  180. var tmp;
  181. this.confid = $(result).find('>conference').attr('id');
  182. var remotecontents = $(result).find('>conference>content').get();
  183. var numparticipants = 0;
  184. for (var i = 0; i < remotecontents.length; i++) {
  185. tmp = $(remotecontents[i]).find('>channel').get();
  186. this.mychannel.push($(tmp.shift()));
  187. numparticipants = tmp.length;
  188. for (j = 0; j < tmp.length; j++) {
  189. if (this.channels[j] === undefined) {
  190. this.channels[j] = [];
  191. }
  192. this.channels[j].push(tmp[j]);
  193. }
  194. }
  195. console.log('remote channels', this.channels);
  196. var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n');
  197. bridgeSDP.media.length = this.mychannel.length;
  198. var channel;
  199. /*
  200. for (channel = 0; channel < bridgeSDP.media.length; channel++) {
  201. bridgeSDP.media[channel] = '';
  202. // unchanged lines
  203. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'm=') + '\r\n';
  204. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'c=') + '\r\n';
  205. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:')) {
  206. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:') + '\r\n';
  207. }
  208. if (SDPUtil.find_line(localSDP.media[channel], 'a=mid:')) {
  209. bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=mid:') + '\r\n';
  210. }
  211. if (SDPUtil.find_line(localSDP.media[channel], 'a=sendrecv')) {
  212. bridgeSDP.media[channel] += 'a=sendrecv\r\n';
  213. }
  214. if (SDPUtil.find_line(localSDP.media[channel], 'a=extmap:')) {
  215. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=extmap:').join('\r\n') + '\r\n';
  216. }
  217. // FIXME: should look at m-line and group the ids together
  218. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtpmap:')) {
  219. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtpmap:').join('\r\n') + '\r\n';
  220. }
  221. if (SDPUtil.find_line(localSDP.media[channel], 'a=fmtp:')) {
  222. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=fmtp:').join('\r\n') + '\r\n';
  223. }
  224. if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp-fb:')) {
  225. bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtcp-fb:').join('\r\n') + '\r\n';
  226. }
  227. // FIXME: changed lines -- a=sendrecv direction, a=setup direction
  228. }
  229. */
  230. for (channel = 0; channel < bridgeSDP.media.length; channel++) {
  231. // get the mixed ssrc
  232. tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  233. // FIXME: check rtp-level-relay-type
  234. if (tmp.length) {
  235. bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
  236. bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
  237. bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
  238. bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
  239. } else {
  240. // make chrome happy... '3735928559' == 0xDEADBEEF
  241. // FIXME: this currently appears as two streams, should be one
  242. bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
  243. bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
  244. bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabel mixedlabelv0' + '\r\n';
  245. bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabel' + '\r\n';
  246. }
  247. // FIXME: should take code from .fromJingle
  248. tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  249. if (tmp.length) {
  250. bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  251. bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  252. tmp.find('>candidate').each(function () {
  253. bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
  254. });
  255. tmp = tmp.find('>fingerprint');
  256. if (tmp.length) {
  257. bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  258. bridgeSDP.media[channel] += 'a=setup:actpass\r\n'; // offer so always actpass
  259. }
  260. }
  261. }
  262. bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
  263. this.peerconnection.setRemoteDescription(
  264. new RTCSessionDescription({type: 'offer', sdp: bridgeSDP.raw}),
  265. function () {
  266. console.log('setRemoteDescription success');
  267. self.peerconnection.createAnswer(
  268. function (answer) {
  269. self.peerconnection.setLocalDescription(answer,
  270. function () {
  271. console.log('setLocalDescription succeded.');
  272. // make sure our presence is updated
  273. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  274. var elem = $iq({to: self.bridgejid, type: 'get'});
  275. elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
  276. var localSDP = new SDP(self.peerconnection.localDescription.sdp);
  277. localSDP.media.forEach(function (media, channel) {
  278. var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
  279. elem.c('content', {name: name});
  280. elem.c('channel', {
  281. initiator: 'true',
  282. expire: '15',
  283. id: self.mychannel[channel].attr('id')
  284. });
  285. // FIXME: should reuse code from .toJingle
  286. var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
  287. for (var j = 0; j < mline.fmt.length; j++) {
  288. var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
  289. elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
  290. elem.up();
  291. }
  292. localSDP.TransportToJingle(channel, elem);
  293. elem.up(); // end of channel
  294. elem.up(); // end of content
  295. });
  296. self.connection.sendIQ(elem,
  297. function (result) {
  298. // ...
  299. },
  300. function (error) {
  301. console.warn(error);
  302. }
  303. );
  304. // now initiate sessions
  305. for (var i = 0; i < numparticipants; i++) {
  306. self.initiate(self.peers[i], true);
  307. }
  308. },
  309. function (error) {
  310. console.warn('setLocalDescription failed.', error);
  311. }
  312. );
  313. },
  314. function (error) {
  315. console.warn('createAnswer failed.', error);
  316. }
  317. );
  318. /*
  319. for (var i = 0; i < numparticipants; i++) {
  320. self.initiate(self.peers[i], true);
  321. }
  322. */
  323. },
  324. function (error) {
  325. console.log('setRemoteDescription failed.', error);
  326. }
  327. );
  328. };
  329. // send a session-initiate to a new participant
  330. ColibriFocus.prototype.initiate = function (peer, isInitiator) {
  331. var participant = this.peers.indexOf(peer);
  332. console.log('tell', peer, participant);
  333. var sdp;
  334. if (this.peerconnection !== null && this.peerconnection.signalingState == 'stable') {
  335. sdp = new SDP(this.peerconnection.remoteDescription.sdp);
  336. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  337. // throw away stuff we don't want
  338. // not needed with static offer
  339. sdp.removeSessionLines('a=group:');
  340. sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
  341. for (var i = 0; i < sdp.media.length; i++) {
  342. sdp.removeMediaLines(i, 'a=rtcp-mux');
  343. sdp.removeMediaLines(i, 'a=ssrc:');
  344. sdp.removeMediaLines(i, 'a=crypto:');
  345. sdp.removeMediaLines(i, 'a=candidate:');
  346. sdp.removeMediaLines(i, 'a=ice-options:google-ice');
  347. sdp.removeMediaLines(i, 'a=ice-ufrag:');
  348. sdp.removeMediaLines(i, 'a=ice-pwd:');
  349. sdp.removeMediaLines(i, 'a=fingerprint:');
  350. sdp.removeMediaLines(i, 'a=setup:');
  351. if (1) { //i > 0) { // not for audio FIXME: does not work as intended
  352. // re-add all remote a=ssrcs
  353. for (var jid in this.remotessrc) {
  354. if (jid == peer) continue;
  355. sdp.media[i] += this.remotessrc[jid][i];
  356. }
  357. // and local a=ssrc lines
  358. sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc').join('\r\n') + '\r\n';
  359. }
  360. }
  361. sdp.raw = sdp.session + sdp.media.join('');
  362. } else {
  363. console.error('can not initiate a new session without a stable peerconnection');
  364. return;
  365. }
  366. // add stuff we got from the bridge
  367. for (var j = 0; j < sdp.media.length; j++) {
  368. var chan = $(this.channels[participant][j]);
  369. console.log('channel id', chan.attr('id'));
  370. tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
  371. if (tmp.length) {
  372. sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
  373. sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
  374. sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
  375. sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
  376. } else {
  377. // make chrome happy... '3735928559' == 0xDEADBEEF
  378. sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
  379. sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
  380. sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabel mixedlabelv0' + '\r\n';
  381. sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabel' + '\r\n';
  382. }
  383. tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
  384. if (tmp.length) {
  385. if (tmp.attr('ufrag'))
  386. sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
  387. if (tmp.attr('pwd'))
  388. sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
  389. // and the candidates...
  390. tmp.find('>candidate').each(function () {
  391. sdp.media[j] += SDPUtil.candidateFromJingle(this);
  392. });
  393. tmp = tmp.find('>fingerprint');
  394. if (tmp.length) {
  395. sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
  396. /*
  397. if (tmp.attr('direction')) {
  398. sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
  399. }
  400. */
  401. sdp.media[j] += 'a=setup:actpass\r\n';
  402. }
  403. }
  404. }
  405. // make a new colibri session and configure it
  406. // FIXME: is it correct to use this.connection.jid when used in a MUC?
  407. var sess = new ColibriSession(this.connection.jid,
  408. Math.random().toString(36).substr(2, 12), // random string
  409. this.connection);
  410. sess.initiate(peer);
  411. sess.colibri = this;
  412. // We do not announce our audio per conference peer, so only video is set here
  413. sess.localVideo = this.connection.jingle.localVideo;
  414. sess.media_constraints = this.connection.jingle.media_constraints;
  415. sess.pc_constraints = this.connection.jingle.pc_constraints;
  416. sess.ice_config = this.connection.jingle.ice_config;
  417. this.connection.jingle.sessions[sess.sid] = sess;
  418. this.connection.jingle.jid2session[sess.peerjid] = sess;
  419. // send a session-initiate
  420. var init = $iq({to: peer, type: 'set'})
  421. .c('jingle',
  422. {xmlns: 'urn:xmpp:jingle:1',
  423. action: 'session-initiate',
  424. initiator: sess.me,
  425. sid: sess.sid
  426. }
  427. );
  428. sdp.toJingle(init, 'initiator');
  429. this.connection.sendIQ(init,
  430. function (res) {
  431. console.log('got result');
  432. },
  433. function (err) {
  434. console.log('got error');
  435. }
  436. );
  437. };
  438. // pull in a new participant into the conference
  439. ColibriFocus.prototype.addNewParticipant = function (peer) {
  440. var self = this;
  441. if (this.confid === 0) {
  442. // bad state
  443. console.log('confid does not exist yet, postponing', peer);
  444. window.setTimeout(function () {
  445. self.addNewParticipant(peer);
  446. }, 250);
  447. return;
  448. }
  449. var index = this.channels.length;
  450. this.channels.push([]);
  451. this.peers.push(peer);
  452. var elem = $iq({to: this.bridgejid, type: 'get'});
  453. elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  454. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  455. localSDP.media.forEach(function (media, channel) {
  456. var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
  457. elem.c('content', {name: name});
  458. elem.c('channel', {initiator: 'true', expire:'15'});
  459. elem.up(); // end of channel
  460. elem.up(); // end of content
  461. });
  462. this.connection.sendIQ(elem,
  463. function (result) {
  464. var contents = $(result).find('>conference>content').get();
  465. for (var i = 0; i < contents.length; i++) {
  466. tmp = $(contents[i]).find('>channel').get();
  467. self.channels[index][i] = tmp[0];
  468. }
  469. self.initiate(peer, true);
  470. },
  471. function (error) {
  472. console.warn(error);
  473. }
  474. );
  475. };
  476. // update the channel description (payload-types + dtls fp) for a participant
  477. ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
  478. console.log('change allocation for', this.confid);
  479. var change = $iq({to: this.bridgejid, type: 'set'});
  480. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  481. for (channel = 0; channel < this.channels[participant].length; channel++) {
  482. change.c('content', {name: channel === 0 ? 'audio' : 'video'});
  483. change.c('channel', {id: $(this.channels[participant][channel]).attr('id')});
  484. var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
  485. rtpmap.forEach(function (val) {
  486. // TODO: too much copy-paste
  487. var rtpmap = SDPUtil.parse_rtpmap(val);
  488. change.c('payload-type', rtpmap);
  489. //
  490. // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
  491. /*
  492. if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
  493. tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
  494. for (var k = 0; k < tmp.length; k++) {
  495. change.c('parameter', tmp[k]).up();
  496. }
  497. }
  498. */
  499. change.up();
  500. });
  501. // now add transport
  502. remoteSDP.TransportToJingle(channel, change);
  503. change.up(); // end of channel
  504. change.up(); // end of content
  505. }
  506. this.connection.sendIQ(change,
  507. function (res) {
  508. console.log('got result');
  509. },
  510. function (err) {
  511. console.log('got error');
  512. }
  513. );
  514. };
  515. // tell everyone about a new participants a=ssrc lines (isadd is true)
  516. // or a leaving participants a=ssrc lines
  517. // FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid
  518. ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
  519. var self = this;
  520. this.peers.forEach(function (peerjid) {
  521. if (peerjid == jid) return;
  522. console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', jid);
  523. if (!self.remotessrc[peerjid]) {
  524. // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
  525. // possibly, this.remoteSSRC[session.peerjid] does not exist yet
  526. console.warn('do we really want to bother', peerjid, 'with updates yet?');
  527. }
  528. var channel;
  529. var peersess = self.connection.jingle.jid2session[peerjid];
  530. var modify = $iq({to: peerjid, type: 'set'})
  531. .c('jingle', {
  532. xmlns: 'urn:xmpp:jingle:1',
  533. action: isadd ? 'addsource' : 'removesource',
  534. initiator: peersess.initiator,
  535. sid: peersess.sid
  536. }
  537. );
  538. // FIXME: only announce video ssrcs since we mix audio and dont need
  539. // the audio ssrcs therefore
  540. var modified = false;
  541. for (channel = 0; channel < sdp.media.length; channel++) {
  542. modified = true;
  543. tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:');
  544. modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))});
  545. modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
  546. // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
  547. tmp.forEach(function (line) {
  548. var idx = line.indexOf(' ');
  549. var linessrc = line.substr(0, idx).substr(7);
  550. modify.attrs({ssrc: linessrc});
  551. var kv = line.substr(idx + 1);
  552. modify.c('parameter');
  553. if (kv.indexOf(':') == -1) {
  554. modify.attrs({ name: kv });
  555. } else {
  556. modify.attrs({ name: kv.split(':', 2)[0] });
  557. modify.attrs({ value: kv.split(':', 2)[1] });
  558. }
  559. modify.up();
  560. });
  561. modify.up(); // end of source
  562. modify.up(); // end of content
  563. }
  564. if (modified) {
  565. self.connection.sendIQ(modify,
  566. function (res) {
  567. console.warn('got modify result');
  568. },
  569. function (err) {
  570. console.warn('got modify error', err);
  571. }
  572. );
  573. } else {
  574. console.log('modification not necessary');
  575. }
  576. });
  577. };
  578. ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
  579. var participant = this.peers.indexOf(session.peerjid);
  580. console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
  581. var self = this;
  582. var remoteSDP = new SDP('');
  583. var tmp;
  584. var channel;
  585. remoteSDP.fromJingle(elem);
  586. // ACT 1: change allocation on bridge
  587. this.updateChannel(remoteSDP, participant);
  588. // ACT 2: tell anyone else about the new SSRCs
  589. this.sendSSRCUpdate(remoteSDP, session.peerjid, true);
  590. // ACT 3: note the SSRCs
  591. this.remotessrc[session.peerjid] = [];
  592. for (channel = 0; channel < this.channels[participant].length; channel++) {
  593. //if (channel == 0) continue; FIXME: does not work as intended
  594. if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
  595. this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
  596. }
  597. }
  598. // ACT 4: add new a=ssrc lines to local remotedescription
  599. for (channel = 0; channel < this.channels[participant].length; channel++) {
  600. //if (channel == 0) continue; FIXME: does not work as intended
  601. if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
  602. this.peerconnection.enqueueAddSsrc(
  603. channel,
  604. SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'
  605. );
  606. }
  607. }
  608. this.modifySources();
  609. };
  610. // relay ice candidates to bridge using trickle
  611. ColibriFocus.prototype.addIceCandidate = function (session, elem) {
  612. var self = this;
  613. var participant = this.peers.indexOf(session.peerjid);
  614. //console.log('change transport allocation for', this.confid, session.peerjid, participant);
  615. var change = $iq({to: this.bridgejid, type: 'set'});
  616. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  617. $(elem).each(function () {
  618. var name = $(this).attr('name');
  619. var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
  620. change.c('content', {name: name});
  621. change.c('channel', {id: $(self.channels[participant][channel]).attr('id')});
  622. $(this).find('>transport').each(function () {
  623. change.c('transport', {
  624. ufrag: $(this).attr('ufrag'),
  625. pwd: $(this).attr('pwd'),
  626. xmlns: $(this).attr('xmlns')
  627. });
  628. $(this).find('>candidate').each(function () {
  629. /* not yet
  630. if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
  631. // chrome generates TCP candidates with port 0
  632. return;
  633. }
  634. */
  635. var line = SDPUtil.candidateFromJingle(this);
  636. change.c('candidate', SDPUtil.candidateToJingle(line)).up();
  637. });
  638. change.up(); // end of transport
  639. });
  640. change.up(); // end of channel
  641. change.up(); // end of content
  642. });
  643. // FIXME: need to check if there is at least one candidate when filtering TCP ones
  644. this.connection.sendIQ(change,
  645. function (res) {
  646. console.log('got result');
  647. },
  648. function (err) {
  649. console.error('got error', err);
  650. }
  651. );
  652. };
  653. // send our own candidate to the bridge
  654. ColibriFocus.prototype.sendIceCandidate = function (candidate) {
  655. var self = this;
  656. //console.log('candidate', candidate);
  657. if (!candidate) {
  658. console.log('end of candidates');
  659. return;
  660. }
  661. if (this.drip_container.length === 0) {
  662. // start 20ms callout
  663. window.setTimeout(function () {
  664. if (self.drip_container.length === 0) return;
  665. self.sendIceCandidates(self.drip_container);
  666. self.drip_container = [];
  667. }, 20);
  668. }
  669. this.drip_container.push(candidate);
  670. };
  671. // sort and send multiple candidates
  672. ColibriFocus.prototype.sendIceCandidates = function (candidates) {
  673. var self = this;
  674. var mycands = $iq({to: this.bridgejid, type: 'set'});
  675. mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  676. // FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
  677. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  678. for (var mid = 0; mid < localSDP.media.length; mid++) {
  679. var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
  680. if (cands.length > 0) {
  681. mycands.c('content', {name: cands[0].sdpMid });
  682. mycands.c('channel', {id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id')});
  683. mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
  684. for (var i = 0; i < cands.length; i++) {
  685. mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
  686. }
  687. mycands.up(); // transport
  688. mycands.up(); // channel
  689. mycands.up(); // content
  690. }
  691. }
  692. console.log('send cands', candidates);
  693. this.connection.sendIQ(mycands,
  694. function (res) {
  695. console.log('got result');
  696. },
  697. function (err) {
  698. console.error('got error', err);
  699. }
  700. );
  701. };
  702. ColibriFocus.prototype.terminate = function (session, reason) {
  703. console.log('remote session terminated from', session.peerjid);
  704. var participant = this.peers.indexOf(session.peerjid);
  705. if (!this.remotessrc[session.peerjid] || participant == -1) {
  706. return;
  707. }
  708. var ssrcs = this.remotessrc[session.peerjid];
  709. for (var i = 0; i < ssrcs.length; i++) {
  710. this.peerconnection.enqueueRemoveSsrc(i, ssrcs[i]);
  711. }
  712. // remove from this.peers
  713. this.peers.splice(participant, 1);
  714. // expire channel on bridge
  715. var change = $iq({to: this.bridgejid, type: 'set'});
  716. change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
  717. for (var channel = 0; channel < this.channels[participant].length; channel++) {
  718. change.c('content', {name: channel === 0 ? 'audio' : 'video'});
  719. change.c('channel', {id: $(this.channels[participant][channel]).attr('id'), expire: '0'});
  720. change.up(); // end of channel
  721. change.up(); // end of content
  722. }
  723. this.connection.sendIQ(change,
  724. function (res) {
  725. console.log('got result');
  726. },
  727. function (err) {
  728. console.error('got error', err);
  729. }
  730. );
  731. // and remove from channels
  732. this.channels.splice(participant, 1);
  733. // tell everyone about the ssrcs to be removed
  734. var sdp = new SDP('');
  735. var localSDP = new SDP(this.peerconnection.localDescription.sdp);
  736. var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
  737. for (var j = 0; j < ssrcs.length; j++) {
  738. sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
  739. sdp.media[j] += ssrcs[j];
  740. this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]);
  741. }
  742. this.sendSSRCUpdate(sdp, session.peerjid, false);
  743. delete this.remotessrc[session.peerjid];
  744. this.modifySources();
  745. };
  746. ColibriFocus.prototype.modifySources = function () {
  747. var self = this;
  748. this.peerconnection.modifySources(function(){
  749. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  750. });
  751. };
  752. ColibriFocus.prototype.hardMuteVideo = function (muted) {
  753. this.peerconnection.hardMuteVideo(muted);
  754. this.connection.jingle.localVideo.getVideoTracks().forEach(function (track) {
  755. track.enabled = !muted;
  756. });
  757. };