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.

RTCUtils.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. /* global config, require, attachMediaStream, getUserMedia */
  2. var RTCBrowserType = require("./RTCBrowserType");
  3. var Resolutions = require("../../service/RTC/Resolutions");
  4. var AdapterJS = require("./adapter.screenshare");
  5. var SDPUtil = require("../xmpp/SDPUtil");
  6. var currentResolution = null;
  7. function getPreviousResolution(resolution) {
  8. if(!Resolutions[resolution])
  9. return null;
  10. var order = Resolutions[resolution].order;
  11. var res = null;
  12. var resName = null;
  13. for(var i in Resolutions) {
  14. var tmp = Resolutions[i];
  15. if(res == null || (res.order < tmp.order && tmp.order < order)) {
  16. resName = i;
  17. res = tmp;
  18. }
  19. }
  20. return resName;
  21. }
  22. function setResolutionConstraints(constraints, resolution, isAndroid) {
  23. if (resolution && !constraints.video || isAndroid) {
  24. // same behaviour as true
  25. constraints.video = { mandatory: {}, optional: [] };
  26. }
  27. if(Resolutions[resolution]) {
  28. constraints.video.mandatory.minWidth = Resolutions[resolution].width;
  29. constraints.video.mandatory.minHeight = Resolutions[resolution].height;
  30. }
  31. else {
  32. if (isAndroid) {
  33. constraints.video.mandatory.minWidth = 320;
  34. constraints.video.mandatory.minHeight = 240;
  35. constraints.video.mandatory.maxFrameRate = 15;
  36. }
  37. }
  38. if (constraints.video.mandatory.minWidth)
  39. constraints.video.mandatory.maxWidth =
  40. constraints.video.mandatory.minWidth;
  41. if (constraints.video.mandatory.minHeight)
  42. constraints.video.mandatory.maxHeight =
  43. constraints.video.mandatory.minHeight;
  44. }
  45. function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid)
  46. {
  47. var constraints = {audio: false, video: false};
  48. if (um.indexOf('video') >= 0) {
  49. // same behaviour as true
  50. constraints.video = { mandatory: {}, optional: [] };
  51. }
  52. if (um.indexOf('audio') >= 0) {
  53. // same behaviour as true
  54. constraints.audio = { mandatory: {}, optional: []};
  55. }
  56. if (um.indexOf('screen') >= 0) {
  57. if (RTCBrowserType.isChrome()) {
  58. constraints.video = {
  59. mandatory: {
  60. chromeMediaSource: "screen",
  61. googLeakyBucket: true,
  62. maxWidth: window.screen.width,
  63. maxHeight: window.screen.height,
  64. maxFrameRate: 3
  65. },
  66. optional: []
  67. };
  68. } else if (RTCBrowserType.isTemasysPluginUsed()) {
  69. constraints.video = {
  70. optional: [
  71. {
  72. sourceId: AdapterJS.WebRTCPlugin.plugin.screensharingKey
  73. }
  74. ]
  75. };
  76. } else {
  77. console.error(
  78. "'screen' WebRTC media source is supported only in Chrome" +
  79. " and with Temasys plugin");
  80. }
  81. }
  82. if (um.indexOf('desktop') >= 0) {
  83. constraints.video = {
  84. mandatory: {
  85. chromeMediaSource: "desktop",
  86. chromeMediaSourceId: desktopStream,
  87. googLeakyBucket: true,
  88. maxWidth: window.screen.width,
  89. maxHeight: window.screen.height,
  90. maxFrameRate: 3
  91. },
  92. optional: []
  93. };
  94. }
  95. if (constraints.audio) {
  96. // if it is good enough for hangouts...
  97. constraints.audio.optional.push(
  98. {googEchoCancellation: true},
  99. {googAutoGainControl: true},
  100. {googNoiseSupression: true},
  101. {googHighpassFilter: true},
  102. {googNoisesuppression2: true},
  103. {googEchoCancellation2: true},
  104. {googAutoGainControl2: true}
  105. );
  106. }
  107. if (constraints.video) {
  108. constraints.video.optional.push(
  109. {googNoiseReduction: false} // chrome 37 workaround for issue 3807, reenable in M38
  110. );
  111. if (um.indexOf('video') >= 0) {
  112. constraints.video.optional.push(
  113. {googLeakyBucket: true}
  114. );
  115. }
  116. }
  117. if (um.indexOf('video') >= 0) {
  118. setResolutionConstraints(constraints, resolution, isAndroid);
  119. }
  120. if (bandwidth) {
  121. if (!constraints.video) {
  122. //same behaviour as true
  123. constraints.video = {mandatory: {}, optional: []};
  124. }
  125. constraints.video.optional.push({bandwidth: bandwidth});
  126. }
  127. if (fps) {
  128. // for some cameras it might be necessary to request 30fps
  129. // so they choose 30fps mjpg over 10fps yuy2
  130. if (!constraints.video) {
  131. // same behaviour as true;
  132. constraints.video = {mandatory: {}, optional: []};
  133. }
  134. constraints.video.mandatory.minFrameRate = fps;
  135. }
  136. return constraints;
  137. }
  138. function RTCUtils(RTCService, onTemasysPluginReady)
  139. {
  140. var self = this;
  141. this.service = RTCService;
  142. if (RTCBrowserType.isFirefox()) {
  143. var FFversion = RTCBrowserType.getFirefoxVersion();
  144. if (FFversion >= 40 && config.useBundle && config.useRtcpMux) {
  145. this.peerconnection = mozRTCPeerConnection;
  146. this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
  147. this.pc_constraints = {};
  148. this.attachMediaStream = function (element, stream) {
  149. // srcObject is being standardized and FF will eventually
  150. // support that unprefixed. FF also supports the
  151. // "element.src = URL.createObjectURL(...)" combo, but that
  152. // will be deprecated in favour of srcObject.
  153. //
  154. // https://groups.google.com/forum/#!topic/mozilla.dev.media/pKOiioXonJg
  155. // https://github.com/webrtc/samples/issues/302
  156. if(!element[0])
  157. return;
  158. element[0].mozSrcObject = stream;
  159. element[0].play();
  160. };
  161. this.getStreamID = function (stream) {
  162. var id = stream.id;
  163. if (!id) {
  164. var tracks = stream.getVideoTracks();
  165. if (!tracks || tracks.length === 0) {
  166. tracks = stream.getAudioTracks();
  167. }
  168. id = tracks[0].id;
  169. }
  170. return SDPUtil.filter_special_chars(id);
  171. };
  172. this.getVideoSrc = function (element) {
  173. if(!element)
  174. return null;
  175. return element.mozSrcObject;
  176. };
  177. this.setVideoSrc = function (element, src) {
  178. if(element)
  179. element.mozSrcObject = src;
  180. };
  181. RTCSessionDescription = mozRTCSessionDescription;
  182. RTCIceCandidate = mozRTCIceCandidate;
  183. } else {
  184. console.error(
  185. "Firefox requirements not met, ver: " + FFversion +
  186. ", bundle: " + config.useBundle +
  187. ", rtcp-mux: " + config.useRtcpMux);
  188. window.location.href = 'unsupported_browser.html';
  189. return;
  190. }
  191. } else if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) {
  192. this.peerconnection = webkitRTCPeerConnection;
  193. this.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
  194. this.attachMediaStream = function (element, stream) {
  195. element.attr('src', webkitURL.createObjectURL(stream));
  196. };
  197. this.getStreamID = function (stream) {
  198. // streams from FF endpoints have the characters '{' and '}'
  199. // that make jQuery choke.
  200. return SDPUtil.filter_special_chars(stream.id);
  201. };
  202. this.getVideoSrc = function (element) {
  203. if(!element)
  204. return null;
  205. return element.getAttribute("src");
  206. };
  207. this.setVideoSrc = function (element, src) {
  208. if(element)
  209. element.setAttribute("src", src);
  210. };
  211. // DTLS should now be enabled by default but..
  212. this.pc_constraints = {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]};
  213. if (navigator.userAgent.indexOf('Android') != -1) {
  214. this.pc_constraints = {}; // disable DTLS on Android
  215. }
  216. if (!webkitMediaStream.prototype.getVideoTracks) {
  217. webkitMediaStream.prototype.getVideoTracks = function () {
  218. return this.videoTracks;
  219. };
  220. }
  221. if (!webkitMediaStream.prototype.getAudioTracks) {
  222. webkitMediaStream.prototype.getAudioTracks = function () {
  223. return this.audioTracks;
  224. };
  225. }
  226. }
  227. // Detect IE/Safari
  228. else if (RTCBrowserType.isTemasysPluginUsed()) {
  229. //AdapterJS.WebRTCPlugin.setLogLevel(
  230. // AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
  231. AdapterJS.webRTCReady(function (isPlugin) {
  232. self.peerconnection = RTCPeerConnection;
  233. self.getUserMedia = getUserMedia;
  234. self.attachMediaStream = function (elSel, stream) {
  235. if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
  236. return;
  237. }
  238. attachMediaStream(elSel[0], stream);
  239. };
  240. self.getStreamID = function (stream) {
  241. var id = SDPUtil.filter_special_chars(stream.label);
  242. return id;
  243. };
  244. self.getVideoSrc = function (element) {
  245. if (!element) {
  246. console.warn("Attempt to get video SRC of null element");
  247. return null;
  248. }
  249. var children = element.children;
  250. for (var i = 0; i !== children.length; ++i) {
  251. if (children[i].name === 'streamId') {
  252. return children[i].value;
  253. }
  254. }
  255. //console.info(element.id + " SRC: " + src);
  256. return null;
  257. };
  258. self.setVideoSrc = function (element, src) {
  259. //console.info("Set video src: ", element, src);
  260. if (!src) {
  261. console.warn("Not attaching video stream, 'src' is null");
  262. return;
  263. }
  264. AdapterJS.WebRTCPlugin.WaitForPluginReady();
  265. var stream = AdapterJS.WebRTCPlugin.plugin
  266. .getStreamWithId(AdapterJS.WebRTCPlugin.pageId, src);
  267. attachMediaStream(element, stream);
  268. };
  269. onTemasysPluginReady(isPlugin);
  270. });
  271. } else {
  272. try {
  273. console.log('Browser does not appear to be WebRTC-capable');
  274. } catch (e) { }
  275. window.location.href = 'unsupported_browser.html';
  276. }
  277. }
  278. RTCUtils.prototype.getUserMediaWithConstraints = function(
  279. um, success_callback, failure_callback, resolution,bandwidth, fps,
  280. desktopStream) {
  281. currentResolution = resolution;
  282. // Check if we are running on Android device
  283. var isAndroid = navigator.userAgent.indexOf('Android') != -1;
  284. var constraints = getConstraints(
  285. um, resolution, bandwidth, fps, desktopStream, isAndroid);
  286. console.info("Get media constraints", constraints);
  287. var self = this;
  288. try {
  289. this.getUserMedia(constraints,
  290. function (stream) {
  291. console.log('onUserMediaSuccess');
  292. self.setAvailableDevices(um, true);
  293. success_callback(stream);
  294. },
  295. function (error) {
  296. self.setAvailableDevices(um, false);
  297. console.warn('Failed to get access to local media. Error ',
  298. error, constraints);
  299. if (failure_callback) {
  300. failure_callback(error);
  301. }
  302. });
  303. } catch (e) {
  304. console.error('GUM failed: ', e);
  305. if(failure_callback) {
  306. failure_callback(e);
  307. }
  308. }
  309. };
  310. RTCUtils.prototype.setAvailableDevices = function (um, available) {
  311. var devices = {};
  312. if(um.indexOf("video") != -1) {
  313. devices.video = available;
  314. }
  315. if(um.indexOf("audio") != -1) {
  316. devices.audio = available;
  317. }
  318. this.service.setDeviceAvailability(devices);
  319. };
  320. /**
  321. * We ask for audio and video combined stream in order to get permissions and
  322. * not to ask twice.
  323. */
  324. RTCUtils.prototype.obtainAudioAndVideoPermissions =
  325. function(devices, callback, usageOptions)
  326. {
  327. var self = this;
  328. // Get AV
  329. var successCallback = function (stream) {
  330. if(callback)
  331. callback(stream, usageOptions);
  332. else
  333. self.successCallback(stream, usageOptions);
  334. };
  335. if(!devices)
  336. devices = ['audio', 'video'];
  337. var newDevices = [];
  338. if(usageOptions)
  339. for(var i = 0; i < devices.length; i++) {
  340. var device = devices[i];
  341. if(usageOptions[device] === true)
  342. newDevices.push(device);
  343. }
  344. else
  345. newDevices = devices;
  346. if(newDevices.length === 0) {
  347. successCallback();
  348. return;
  349. }
  350. if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) {
  351. // With FF/IE we can't split the stream into audio and video because FF
  352. // doesn't support media stream constructors. So, we need to get the
  353. // audio stream separately from the video stream using two distinct GUM
  354. // calls. Not very user friendly :-( but we don't have many other
  355. // options neither.
  356. //
  357. // Note that we pack those 2 streams in a single object and pass it to
  358. // the successCallback method.
  359. var obtainVideo = function (audioStream) {
  360. self.getUserMediaWithConstraints(
  361. ['video'],
  362. function (videoStream) {
  363. return successCallback({
  364. audioStream: audioStream,
  365. videoStream: videoStream
  366. });
  367. },
  368. function (error) {
  369. console.error(
  370. 'failed to obtain video stream - stop', error);
  371. self.errorCallback(error);
  372. },
  373. config.resolution || '360');
  374. };
  375. var obtainAudio = function () {
  376. self.getUserMediaWithConstraints(
  377. ['audio'],
  378. function (audioStream) {
  379. if (newDevices.indexOf('video') !== -1)
  380. obtainVideo(audioStream);
  381. },
  382. function (error) {
  383. console.error(
  384. 'failed to obtain audio stream - stop', error);
  385. self.errorCallback(error);
  386. }
  387. );
  388. };
  389. if (newDevices.indexOf('audio') !== -1) {
  390. obtainAudio();
  391. } else {
  392. obtainVideo(null);
  393. }
  394. } else {
  395. this.getUserMediaWithConstraints(
  396. newDevices,
  397. function (stream) {
  398. successCallback(stream);
  399. },
  400. function (error) {
  401. self.errorCallback(error);
  402. },
  403. config.resolution || '360');
  404. }
  405. };
  406. RTCUtils.prototype.successCallback = function (stream, usageOptions) {
  407. // If this is FF or IE, the stream parameter is *not* a MediaStream object,
  408. // it's an object with two properties: audioStream, videoStream.
  409. if (stream && stream.getAudioTracks && stream.getVideoTracks)
  410. console.log('got', stream, stream.getAudioTracks().length,
  411. stream.getVideoTracks().length);
  412. this.handleLocalStream(stream, usageOptions);
  413. };
  414. RTCUtils.prototype.errorCallback = function (error) {
  415. var self = this;
  416. console.error('failed to obtain audio/video stream - trying audio only', error);
  417. var resolution = getPreviousResolution(currentResolution);
  418. if(typeof error == "object" && error.constraintName && error.name
  419. && (error.name == "ConstraintNotSatisfiedError" ||
  420. error.name == "OverconstrainedError") &&
  421. (error.constraintName == "minWidth" || error.constraintName == "maxWidth" ||
  422. error.constraintName == "minHeight" || error.constraintName == "maxHeight")
  423. && resolution != null)
  424. {
  425. self.getUserMediaWithConstraints(['audio', 'video'],
  426. function (stream) {
  427. return self.successCallback(stream);
  428. }, function (error) {
  429. return self.errorCallback(error);
  430. }, resolution);
  431. }
  432. else {
  433. self.getUserMediaWithConstraints(
  434. ['audio'],
  435. function (stream) {
  436. return self.successCallback(stream);
  437. },
  438. function (error) {
  439. console.error('failed to obtain audio/video stream - stop',
  440. error);
  441. return self.successCallback(null);
  442. }
  443. );
  444. }
  445. };
  446. RTCUtils.prototype.handleLocalStream = function(stream, usageOptions) {
  447. // If this is FF, the stream parameter is *not* a MediaStream object, it's
  448. // an object with two properties: audioStream, videoStream.
  449. var audioStream, videoStream;
  450. if(window.webkitMediaStream)
  451. {
  452. audioStream = new webkitMediaStream();
  453. videoStream = new webkitMediaStream();
  454. if(stream) {
  455. var audioTracks = stream.getAudioTracks();
  456. for (var i = 0; i < audioTracks.length; i++) {
  457. audioStream.addTrack(audioTracks[i]);
  458. }
  459. var videoTracks = stream.getVideoTracks();
  460. for (i = 0; i < videoTracks.length; i++) {
  461. videoStream.addTrack(videoTracks[i]);
  462. }
  463. }
  464. }
  465. else if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed())
  466. { // Firefox and Temasys plugin
  467. if (stream && stream.audioStream)
  468. audioStream = stream.audioStream;
  469. else
  470. audioStream = new DummyMediaStream("dummyAudio");
  471. if (stream && stream.videoStream)
  472. videoStream = stream.videoStream;
  473. else
  474. videoStream = new DummyMediaStream("dummyVideo");
  475. }
  476. var audioMuted = (usageOptions && usageOptions.audio === false),
  477. videoMuted = (usageOptions && usageOptions.video === false);
  478. var audioGUM = (!usageOptions || usageOptions.audio !== false),
  479. videoGUM = (!usageOptions || usageOptions.video !== false);
  480. this.service.createLocalStream(audioStream, "audio", null, null,
  481. audioMuted, audioGUM);
  482. this.service.createLocalStream(videoStream, "video", null, null,
  483. videoMuted, videoGUM);
  484. };
  485. function DummyMediaStream(id) {
  486. this.id = id;
  487. this.label = id;
  488. this.stop = function() { };
  489. this.getAudioTracks = function() { return []; };
  490. this.getVideoTracks = function() { return []; };
  491. }
  492. RTCUtils.prototype.createStream = function(stream, isVideo) {
  493. var newStream = null;
  494. if (window.webkitMediaStream) {
  495. newStream = new webkitMediaStream();
  496. if (newStream) {
  497. var tracks = (isVideo ? stream.getVideoTracks() : stream.getAudioTracks());
  498. for (var i = 0; i < tracks.length; i++) {
  499. newStream.addTrack(tracks[i]);
  500. }
  501. }
  502. }
  503. else {
  504. // FIXME: this is duplicated with 'handleLocalStream' !!!
  505. if (stream) {
  506. newStream = stream;
  507. } else {
  508. newStream =
  509. new DummyMediaStream(isVideo ? "dummyVideo" : "dummyAudio");
  510. }
  511. }
  512. return newStream;
  513. };
  514. module.exports = RTCUtils;