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.

ScreenObtainer.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import JitsiTrackError from '../../JitsiTrackError';
  2. import * as JitsiTrackErrors from '../../JitsiTrackErrors';
  3. import browser from '../browser';
  4. const logger = require('jitsi-meet-logger').getLogger(__filename);
  5. let gumFunction = null;
  6. /**
  7. * Handles obtaining a stream from a screen capture on different browsers.
  8. */
  9. const ScreenObtainer = {
  10. /**
  11. * If not <tt>null</tt> it means that the initialization process is still in
  12. * progress. It is used to make desktop stream request wait and continue
  13. * after it's done.
  14. * {@type Promise|null}
  15. */
  16. obtainStream: null,
  17. /**
  18. * Initializes the function used to obtain a screen capture
  19. * (this.obtainStream).
  20. *
  21. * @param {object} options
  22. * @param {Function} gum GUM method
  23. */
  24. init(options = {}, gum) {
  25. this.options = options;
  26. gumFunction = gum;
  27. this.obtainStream = this._createObtainStreamMethod();
  28. if (!this.obtainStream) {
  29. logger.info('Desktop sharing disabled');
  30. }
  31. },
  32. /**
  33. * Returns a method which will be used to obtain the screen sharing stream
  34. * (based on the browser type).
  35. *
  36. * @returns {Function}
  37. * @private
  38. */
  39. _createObtainStreamMethod() {
  40. if (browser.isNWJS()) {
  41. return (_, onSuccess, onFailure) => {
  42. window.JitsiMeetNW.obtainDesktopStream(
  43. onSuccess,
  44. (error, constraints) => {
  45. let jitsiError;
  46. // FIXME:
  47. // This is very very dirty fix for recognising that the
  48. // user have clicked the cancel button from the Desktop
  49. // sharing pick window. The proper solution would be to
  50. // detect this in the NWJS application by checking the
  51. // streamId === "". Even better solution would be to
  52. // stop calling GUM from the NWJS app and just pass the
  53. // streamId to lib-jitsi-meet. This way the desktop
  54. // sharing implementation for NWJS and chrome extension
  55. // will be the same and lib-jitsi-meet will be able to
  56. // control the constraints, check the streamId, etc.
  57. //
  58. // I cannot find documentation about "InvalidStateError"
  59. // but this is what we are receiving from GUM when the
  60. // streamId for the desktop sharing is "".
  61. if (error && error.name === 'InvalidStateError') {
  62. jitsiError = new JitsiTrackError(
  63. JitsiTrackErrors.SCREENSHARING_USER_CANCELED
  64. );
  65. } else {
  66. jitsiError = new JitsiTrackError(
  67. error, constraints, [ 'desktop' ]);
  68. }
  69. (typeof onFailure === 'function')
  70. && onFailure(jitsiError);
  71. });
  72. };
  73. } else if (browser.isElectron()) {
  74. return this.obtainScreenOnElectron;
  75. } else if (browser.isReactNative() && browser.supportsGetDisplayMedia()) {
  76. return this.obtainScreenFromGetDisplayMediaRN;
  77. } else if (browser.supportsGetDisplayMedia()) {
  78. return this.obtainScreenFromGetDisplayMedia;
  79. }
  80. logger.log('Screen sharing not supported on ', browser.getName());
  81. return null;
  82. },
  83. /**
  84. * Checks whether obtaining a screen capture is supported in the current
  85. * environment.
  86. * @returns {boolean}
  87. */
  88. isSupported() {
  89. return this.obtainStream !== null;
  90. },
  91. /**
  92. * Obtains a screen capture stream on Electron.
  93. *
  94. * @param {Object} [options] - Screen sharing options.
  95. * @param {Array<string>} [options.desktopSharingSources] - Array with the
  96. * sources that have to be displayed in the desktop picker window ('screen',
  97. * 'window', etc.).
  98. * @param onSuccess - Success callback.
  99. * @param onFailure - Failure callback.
  100. */
  101. obtainScreenOnElectron(options = {}, onSuccess, onFailure) {
  102. if (window.JitsiMeetScreenObtainer
  103. && window.JitsiMeetScreenObtainer.openDesktopPicker) {
  104. const { desktopSharingSources, gumOptions } = options;
  105. window.JitsiMeetScreenObtainer.openDesktopPicker(
  106. {
  107. desktopSharingSources: desktopSharingSources || [ 'screen', 'window' ]
  108. },
  109. (streamId, streamType, screenShareAudio = false) =>
  110. onGetStreamResponse(
  111. {
  112. response: {
  113. streamId,
  114. streamType,
  115. screenShareAudio
  116. },
  117. gumOptions
  118. },
  119. onSuccess,
  120. onFailure
  121. ),
  122. err => onFailure(new JitsiTrackError(
  123. JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_ERROR,
  124. err
  125. ))
  126. );
  127. } else {
  128. onFailure(new JitsiTrackError(
  129. JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_NOT_FOUND));
  130. }
  131. },
  132. /**
  133. * Obtains a screen capture stream using getDisplayMedia.
  134. *
  135. * @param callback - The success callback.
  136. * @param errorCallback - The error callback.
  137. */
  138. obtainScreenFromGetDisplayMedia(options, callback, errorCallback) {
  139. logger.info('Using getDisplayMedia for screen sharing');
  140. let getDisplayMedia;
  141. if (navigator.getDisplayMedia) {
  142. getDisplayMedia = navigator.getDisplayMedia.bind(navigator);
  143. } else {
  144. // eslint-disable-next-line max-len
  145. getDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices);
  146. }
  147. const { disableAGC, disableAP, enableHdAudio } = this.options;
  148. const audioProcessingValue = !disableAGC && !disableAP;
  149. const audio = enableHdAudio ? {
  150. autoGainControl: audioProcessingValue,
  151. channelCount: 2,
  152. echoCancellation: audioProcessingValue,
  153. noiseSuppression: audioProcessingValue
  154. } : true;
  155. getDisplayMedia({
  156. video: true,
  157. audio,
  158. cursor: 'always'
  159. })
  160. .then(stream => {
  161. let applyConstraintsPromise;
  162. if (stream
  163. && stream.getTracks()
  164. && stream.getTracks().length > 0) {
  165. const videoTrack = stream.getVideoTracks()[0];
  166. // Apply video track constraint.
  167. if (videoTrack) {
  168. applyConstraintsPromise = videoTrack.applyConstraints(options.trackOptions);
  169. }
  170. } else {
  171. applyConstraintsPromise = Promise.resolve();
  172. }
  173. applyConstraintsPromise.then(() =>
  174. callback({
  175. stream,
  176. sourceId: stream.id
  177. }));
  178. })
  179. .catch(error => {
  180. const errorDetails = {
  181. errorName: error && error.name,
  182. errorMsg: error && error.message,
  183. errorStack: error && error.stack
  184. };
  185. logger.error('getDisplayMedia error', errorDetails);
  186. if (errorDetails.errorMsg && errorDetails.errorMsg.indexOf('denied by system') !== -1) {
  187. // On Chrome this is the only thing different between error returned when user cancels
  188. // and when no permission was given on the OS level.
  189. errorCallback(new JitsiTrackError(JitsiTrackErrors.PERMISSION_DENIED));
  190. return;
  191. }
  192. errorCallback(new JitsiTrackError(JitsiTrackErrors.SCREENSHARING_USER_CANCELED));
  193. });
  194. },
  195. /**
  196. * Obtains a screen capture stream using getDisplayMedia.
  197. *
  198. * @param callback - The success callback.
  199. * @param errorCallback - The error callback.
  200. */
  201. obtainScreenFromGetDisplayMediaRN(options, callback, errorCallback) {
  202. logger.info('Using getDisplayMedia for screen sharing');
  203. navigator.mediaDevices.getDisplayMedia({ video: true })
  204. .then(stream => {
  205. callback({
  206. stream,
  207. sourceId: stream.id });
  208. })
  209. .catch(() => {
  210. errorCallback(new JitsiTrackError(JitsiTrackErrors
  211. .SCREENSHARING_USER_CANCELED));
  212. });
  213. }
  214. };
  215. /**
  216. * Handles response from external application / extension and calls GUM to
  217. * receive the desktop streams or reports error.
  218. * @param {object} options
  219. * @param {object} options.response
  220. * @param {string} options.response.streamId - the streamId for the desktop
  221. * stream.
  222. * @param {bool} options.response.screenShareAudio - Used by electron clients to
  223. * enable system audio screen sharing.
  224. * @param {string} options.response.error - error to be reported.
  225. * @param {object} options.gumOptions - options passed to GUM.
  226. * @param {Function} onSuccess - callback for success.
  227. * @param {Function} onFailure - callback for failure.
  228. * @param {object} gumOptions - options passed to GUM.
  229. */
  230. function onGetStreamResponse(
  231. options = {
  232. response: {},
  233. gumOptions: {}
  234. },
  235. onSuccess,
  236. onFailure) {
  237. const { streamId, streamType, screenShareAudio, error } = options.response || {};
  238. if (streamId) {
  239. const gumOptions = {
  240. desktopStream: streamId,
  241. screenShareAudio,
  242. ...options.gumOptions
  243. };
  244. gumFunction([ 'desktop' ], gumOptions)
  245. .then(stream => onSuccess({
  246. stream,
  247. sourceId: streamId,
  248. sourceType: streamType
  249. }), onFailure);
  250. } else {
  251. // As noted in Chrome Desktop Capture API:
  252. // If user didn't select any source (i.e. canceled the prompt)
  253. // then the callback is called with an empty streamId.
  254. if (streamId === '') {
  255. onFailure(new JitsiTrackError(
  256. JitsiTrackErrors.SCREENSHARING_USER_CANCELED));
  257. return;
  258. }
  259. onFailure(new JitsiTrackError(
  260. JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR,
  261. error));
  262. }
  263. }
  264. export default ScreenObtainer;