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

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