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

TraceablePeerConnection.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. /* global $ */
  2. var RTC = require('../RTC/RTC');
  3. var logger = require("jitsi-meet-logger").getLogger(__filename);
  4. var RTCBrowserType = require("../RTC/RTCBrowserType.js");
  5. var XMPPEvents = require("../../service/xmpp/XMPPEvents");
  6. function TraceablePeerConnection(ice_config, constraints, session) {
  7. var self = this;
  8. this.session = session;
  9. this.recvOnlySSRCs = {};
  10. var RTCPeerConnectionType = null;
  11. if (RTCBrowserType.isFirefox()) {
  12. RTCPeerConnectionType = mozRTCPeerConnection;
  13. } else if (RTCBrowserType.isTemasysPluginUsed()) {
  14. RTCPeerConnectionType = RTCPeerConnection;
  15. } else {
  16. RTCPeerConnectionType = webkitRTCPeerConnection;
  17. }
  18. this.peerconnection = new RTCPeerConnectionType(ice_config, constraints);
  19. this.updateLog = [];
  20. this.stats = {};
  21. this.statsinterval = null;
  22. this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
  23. var Interop = require('sdp-interop').Interop;
  24. this.interop = new Interop();
  25. var Simulcast = require('sdp-simulcast');
  26. this.simulcast = new Simulcast({numOfLayers: 3,
  27. explodeRemoteSimulcast: false});
  28. this.eventEmitter = this.session.room.eventEmitter;
  29. // override as desired
  30. this.trace = function (what, info) {
  31. /*logger.warn('WTRACE', what, info);
  32. if (info && RTCBrowserType.isIExplorer()) {
  33. if (info.length > 1024) {
  34. logger.warn('WTRACE', what, info.substr(1024));
  35. }
  36. if (info.length > 2048) {
  37. logger.warn('WTRACE', what, info.substr(2048));
  38. }
  39. }*/
  40. self.updateLog.push({
  41. time: new Date(),
  42. type: what,
  43. value: info || ""
  44. });
  45. };
  46. this.onicecandidate = null;
  47. this.peerconnection.onicecandidate = function (event) {
  48. // FIXME: this causes stack overflow with Temasys Plugin
  49. if (!RTCBrowserType.isTemasysPluginUsed())
  50. self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
  51. if (self.onicecandidate !== null) {
  52. self.onicecandidate(event);
  53. }
  54. };
  55. this.onaddstream = null;
  56. this.peerconnection.onaddstream = function (event) {
  57. self.trace('onaddstream', event.stream.id);
  58. if (self.onaddstream !== null) {
  59. self.onaddstream(event);
  60. }
  61. };
  62. this.onremovestream = null;
  63. this.peerconnection.onremovestream = function (event) {
  64. self.trace('onremovestream', event.stream.id);
  65. if (self.onremovestream !== null) {
  66. self.onremovestream(event);
  67. }
  68. };
  69. this.onsignalingstatechange = null;
  70. this.peerconnection.onsignalingstatechange = function (event) {
  71. self.trace('onsignalingstatechange', self.signalingState);
  72. if (self.onsignalingstatechange !== null) {
  73. self.onsignalingstatechange(event);
  74. }
  75. };
  76. this.oniceconnectionstatechange = null;
  77. this.peerconnection.oniceconnectionstatechange = function (event) {
  78. self.trace('oniceconnectionstatechange', self.iceConnectionState);
  79. if (self.oniceconnectionstatechange !== null) {
  80. self.oniceconnectionstatechange(event);
  81. }
  82. };
  83. this.onnegotiationneeded = null;
  84. this.peerconnection.onnegotiationneeded = function (event) {
  85. self.trace('onnegotiationneeded');
  86. if (self.onnegotiationneeded !== null) {
  87. self.onnegotiationneeded(event);
  88. }
  89. };
  90. self.ondatachannel = null;
  91. this.peerconnection.ondatachannel = function (event) {
  92. self.trace('ondatachannel', event);
  93. if (self.ondatachannel !== null) {
  94. self.ondatachannel(event);
  95. }
  96. };
  97. // XXX: do all non-firefox browsers which we support also support this?
  98. if (!RTCBrowserType.isFirefox() && this.maxstats) {
  99. this.statsinterval = window.setInterval(function() {
  100. self.peerconnection.getStats(function(stats) {
  101. var results = stats.result();
  102. var now = new Date();
  103. for (var i = 0; i < results.length; ++i) {
  104. results[i].names().forEach(function (name) {
  105. var id = results[i].id + '-' + name;
  106. if (!self.stats[id]) {
  107. self.stats[id] = {
  108. startTime: now,
  109. endTime: now,
  110. values: [],
  111. times: []
  112. };
  113. }
  114. self.stats[id].values.push(results[i].stat(name));
  115. self.stats[id].times.push(now.getTime());
  116. if (self.stats[id].values.length > self.maxstats) {
  117. self.stats[id].values.shift();
  118. self.stats[id].times.shift();
  119. }
  120. self.stats[id].endTime = now;
  121. });
  122. }
  123. });
  124. }, 1000);
  125. }
  126. }
  127. /**
  128. * Returns a string representation of a SessionDescription object.
  129. */
  130. var dumpSDP = function(description) {
  131. if (typeof description === 'undefined' || description == null) {
  132. return '';
  133. }
  134. return 'type: ' + description.type + '\r\n' + description.sdp;
  135. };
  136. /**
  137. * Injects receive only SSRC in the sdp if there are not other SSRCs.
  138. * @param desc the SDP that will be modified.
  139. * @returns the modified SDP.
  140. */
  141. TraceablePeerConnection.prototype.insertRecvOnlySSRC = function (desc) {
  142. if (typeof desc !== 'object' || desc === null ||
  143. typeof desc.sdp !== 'string') {
  144. console.warn('An empty description was passed as an argument.');
  145. return desc;
  146. }
  147. var transform = require('sdp-transform');
  148. var RandomUtil = require('../util/RandomUtil');
  149. var session = transform.parse(desc.sdp);
  150. if (!Array.isArray(session.media))
  151. {
  152. return;
  153. }
  154. var modded = false;
  155. session.media.forEach(function (bLine) {
  156. if (bLine.direction != 'recvonly')
  157. {
  158. return;
  159. }
  160. modded = true;
  161. if (!Array.isArray(bLine.ssrcs) || bLine.ssrcs.length === 0)
  162. {
  163. var ssrc = this.recvOnlySSRCs[bLine.type]
  164. = this.recvOnlySSRCs[bLine.type] ||
  165. RandomUtil.randomInt(1, 0xffffffff);
  166. bLine.ssrcs = [{
  167. id: ssrc,
  168. attribute: 'cname',
  169. value: ['recvonly-', ssrc].join('')
  170. }];
  171. }
  172. }.bind(this));
  173. return (!modded) ? desc : new RTCSessionDescription({
  174. type: desc.type,
  175. sdp: transform.write(session),
  176. });
  177. };
  178. /**
  179. * Takes a SessionDescription object and returns a "normalized" version.
  180. * Currently it only takes care of ordering the a=ssrc lines.
  181. */
  182. var normalizePlanB = function(desc) {
  183. if (typeof desc !== 'object' || desc === null ||
  184. typeof desc.sdp !== 'string') {
  185. logger.warn('An empty description was passed as an argument.');
  186. return desc;
  187. }
  188. var transform = require('sdp-transform');
  189. var session = transform.parse(desc.sdp);
  190. if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
  191. Array.isArray(session.media)) {
  192. session.media.forEach(function (mLine) {
  193. // Chrome appears to be picky about the order in which a=ssrc lines
  194. // are listed in an m-line when rtx is enabled (and thus there are
  195. // a=ssrc-group lines with FID semantics). Specifically if we have
  196. // "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
  197. // the "a=ssrc:S1" lines, SRD fails.
  198. // So, put SSRC which appear as the first SSRC in an FID ssrc-group
  199. // first.
  200. var firstSsrcs = [];
  201. var newSsrcLines = [];
  202. if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) {
  203. mLine.ssrcGroups.forEach(function (group) {
  204. if (typeof group.semantics !== 'undefined' &&
  205. group.semantics === 'FID') {
  206. if (typeof group.ssrcs !== 'undefined') {
  207. firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
  208. }
  209. }
  210. });
  211. }
  212. if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
  213. var i;
  214. for (i = 0; i<mLine.ssrcs.length; i++){
  215. if (typeof mLine.ssrcs[i] === 'object'
  216. && typeof mLine.ssrcs[i].id !== 'undefined'
  217. && !$.inArray(mLine.ssrcs[i].id, firstSsrcs)) {
  218. newSsrcLines.push(mLine.ssrcs[i]);
  219. delete mLine.ssrcs[i];
  220. }
  221. }
  222. for (i = 0; i<mLine.ssrcs.length; i++){
  223. if (typeof mLine.ssrcs[i] !== 'undefined') {
  224. newSsrcLines.push(mLine.ssrcs[i]);
  225. }
  226. }
  227. mLine.ssrcs = newSsrcLines;
  228. }
  229. });
  230. }
  231. var resStr = transform.write(session);
  232. return new RTCSessionDescription({
  233. type: desc.type,
  234. sdp: resStr
  235. });
  236. };
  237. var getters = {
  238. signalingState: function () {
  239. return this.peerconnection.signalingState;
  240. },
  241. iceConnectionState: function () {
  242. return this.peerconnection.iceConnectionState;
  243. },
  244. localDescription: function() {
  245. var desc = this.peerconnection.localDescription;
  246. this.trace('getLocalDescription::preTransform', dumpSDP(desc));
  247. // if we're running on FF, transform to Plan B first.
  248. if (RTCBrowserType.usesUnifiedPlan()) {
  249. desc = this.interop.toPlanB(desc);
  250. this.trace('getLocalDescription::postTransform (Plan B)',
  251. dumpSDP(desc));
  252. }
  253. return desc;
  254. },
  255. remoteDescription: function() {
  256. var desc = this.peerconnection.remoteDescription;
  257. this.trace('getRemoteDescription::preTransform', dumpSDP(desc));
  258. // if we're running on FF, transform to Plan B first.
  259. if (RTCBrowserType.usesUnifiedPlan()) {
  260. desc = this.interop.toPlanB(desc);
  261. this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
  262. }
  263. return desc;
  264. }
  265. };
  266. Object.keys(getters).forEach(function (prop) {
  267. Object.defineProperty(
  268. TraceablePeerConnection.prototype,
  269. prop, {
  270. get: getters[prop]
  271. }
  272. );
  273. });
  274. TraceablePeerConnection.prototype.addStream = function (stream) {
  275. this.trace('addStream', stream.id);
  276. try
  277. {
  278. this.peerconnection.addStream(stream);
  279. }
  280. catch (e)
  281. {
  282. logger.error(e);
  283. }
  284. };
  285. TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
  286. this.trace('removeStream', stream.id);
  287. if(stopStreams) {
  288. RTC.stopMediaStream(stream);
  289. }
  290. try {
  291. // FF doesn't support this yet.
  292. if (this.peerconnection.removeStream)
  293. this.peerconnection.removeStream(stream);
  294. } catch (e) {
  295. logger.error(e);
  296. }
  297. };
  298. TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
  299. this.trace('createDataChannel', label, opts);
  300. return this.peerconnection.createDataChannel(label, opts);
  301. };
  302. TraceablePeerConnection.prototype.setLocalDescription
  303. = function (description, successCallback, failureCallback) {
  304. this.trace('setLocalDescription::preTransform', dumpSDP(description));
  305. // if we're running on FF, transform to Plan A first.
  306. if (RTCBrowserType.usesUnifiedPlan()) {
  307. description = this.interop.toUnifiedPlan(description);
  308. this.trace('setLocalDescription::postTransform (Plan A)',
  309. dumpSDP(description));
  310. }
  311. var self = this;
  312. this.peerconnection.setLocalDescription(description,
  313. function () {
  314. self.trace('setLocalDescriptionOnSuccess');
  315. successCallback();
  316. },
  317. function (err) {
  318. self.trace('setLocalDescriptionOnFailure', err);
  319. self.eventEmitter.emit(XMPPEvents.SET_LOCAL_DESCRIPTION_FAILED,
  320. err, self.peerconnection);
  321. failureCallback(err);
  322. }
  323. );
  324. };
  325. TraceablePeerConnection.prototype.setRemoteDescription
  326. = function (description, successCallback, failureCallback) {
  327. this.trace('setRemoteDescription::preTransform', dumpSDP(description));
  328. // TODO the focus should squeze or explode the remote simulcast
  329. description = this.simulcast.mungeRemoteDescription(description);
  330. this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
  331. // if we're running on FF, transform to Plan A first.
  332. if (RTCBrowserType.usesUnifiedPlan()) {
  333. description = this.interop.toUnifiedPlan(description);
  334. this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
  335. }
  336. if (RTCBrowserType.usesPlanB()) {
  337. description = normalizePlanB(description);
  338. }
  339. var self = this;
  340. this.peerconnection.setRemoteDescription(description,
  341. function () {
  342. self.trace('setRemoteDescriptionOnSuccess');
  343. successCallback();
  344. },
  345. function (err) {
  346. self.trace('setRemoteDescriptionOnFailure', err);
  347. self.eventEmitter.emit(XMPPEvents.SET_REMOTE_DESCRIPTION_FAILED,
  348. err, self.peerconnection);
  349. failureCallback(err);
  350. }
  351. );
  352. /*
  353. if (this.statsinterval === null && this.maxstats > 0) {
  354. // start gathering stats
  355. }
  356. */
  357. };
  358. TraceablePeerConnection.prototype.close = function () {
  359. this.trace('stop');
  360. if (this.statsinterval !== null) {
  361. window.clearInterval(this.statsinterval);
  362. this.statsinterval = null;
  363. }
  364. this.peerconnection.close();
  365. };
  366. TraceablePeerConnection.prototype.createOffer
  367. = function (successCallback, failureCallback, constraints) {
  368. var self = this;
  369. this.trace('createOffer', JSON.stringify(constraints, null, ' '));
  370. this.peerconnection.createOffer(
  371. function (offer) {
  372. self.trace('createOfferOnSuccess::preTransform', dumpSDP(offer));
  373. // NOTE this is not tested because in meet the focus generates the
  374. // offer.
  375. // if we're running on FF, transform to Plan B first.
  376. if (RTCBrowserType.usesUnifiedPlan()) {
  377. offer = self.interop.toPlanB(offer);
  378. self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
  379. }
  380. if (RTCBrowserType.isChrome())
  381. {
  382. offer = self.insertRecvOnlySSRC(offer);
  383. self.trace('createAnswerOnSuccess::mungeLocalVideoSSRC',
  384. dumpSDP(offer));
  385. }
  386. if (!self.session.room.options.disableSimulcast
  387. && self.simulcast.isSupported()) {
  388. offer = self.simulcast.mungeLocalDescription(offer);
  389. self.trace('createOfferOnSuccess::postTransform (simulcast)',
  390. dumpSDP(offer));
  391. }
  392. successCallback(offer);
  393. },
  394. function(err) {
  395. self.trace('createOfferOnFailure', err);
  396. self.eventEmitter.emit(XMPPEvents.CREATE_OFFER_FAILED, err,
  397. self.peerconnection);
  398. failureCallback(err);
  399. },
  400. constraints
  401. );
  402. };
  403. TraceablePeerConnection.prototype.createAnswer
  404. = function (successCallback, failureCallback, constraints) {
  405. var self = this;
  406. this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
  407. this.peerconnection.createAnswer(
  408. function (answer) {
  409. self.trace('createAnswerOnSuccess::preTransform', dumpSDP(answer));
  410. // if we're running on FF, transform to Plan A first.
  411. if (RTCBrowserType.usesUnifiedPlan()) {
  412. answer = self.interop.toPlanB(answer);
  413. self.trace('createAnswerOnSuccess::postTransform (Plan B)',
  414. dumpSDP(answer));
  415. }
  416. if (RTCBrowserType.isChrome())
  417. {
  418. answer = self.insertRecvOnlySSRC(answer);
  419. self.trace('createAnswerOnSuccess::mungeLocalVideoSSRC',
  420. dumpSDP(answer));
  421. }
  422. if (!self.session.room.options.disableSimulcast
  423. && self.simulcast.isSupported()) {
  424. answer = self.simulcast.mungeLocalDescription(answer);
  425. self.trace('createAnswerOnSuccess::postTransform (simulcast)',
  426. dumpSDP(answer));
  427. }
  428. successCallback(answer);
  429. },
  430. function(err) {
  431. self.trace('createAnswerOnFailure', err);
  432. self.eventEmitter.emit(XMPPEvents.CREATE_ANSWER_FAILED, err,
  433. self.peerconnection);
  434. failureCallback(err);
  435. },
  436. constraints
  437. );
  438. };
  439. TraceablePeerConnection.prototype.addIceCandidate
  440. = function (candidate, successCallback, failureCallback) {
  441. //var self = this;
  442. this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
  443. this.peerconnection.addIceCandidate(candidate);
  444. /* maybe later
  445. this.peerconnection.addIceCandidate(candidate,
  446. function () {
  447. self.trace('addIceCandidateOnSuccess');
  448. successCallback();
  449. },
  450. function (err) {
  451. self.trace('addIceCandidateOnFailure', err);
  452. failureCallback(err);
  453. }
  454. );
  455. */
  456. };
  457. TraceablePeerConnection.prototype.getStats = function(callback, errback) {
  458. // TODO: Is this the correct way to handle Opera, Temasys?
  459. if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) {
  460. // ignore for now...
  461. if(!errback)
  462. errback = function () {};
  463. this.peerconnection.getStats(null, callback, errback);
  464. } else {
  465. this.peerconnection.getStats(callback);
  466. }
  467. };
  468. module.exports = TraceablePeerConnection;