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

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