Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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