123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
-
- import JitsiTrackError from '../../JitsiTrackError';
- import * as JitsiTrackErrors from '../../JitsiTrackErrors';
- import browser from '../browser';
-
- const logger = require('@jitsi/logger').getLogger(__filename);
-
- /**
- * The default frame rate for Screen Sharing.
- */
- export const SS_DEFAULT_FRAME_RATE = 5;
-
- /**
- * Handles obtaining a stream from a screen capture on different browsers.
- */
- const ScreenObtainer = {
- /**
- * If not <tt>null</tt> it means that the initialization process is still in
- * progress. It is used to make desktop stream request wait and continue
- * after it's done.
- * {@type Promise|null}
- */
-
- obtainStream: null,
-
- /**
- * Initializes the function used to obtain a screen capture
- * (this.obtainStream).
- *
- * @param {object} options
- */
- init(options = {}) {
- this.options = options;
- this.obtainStream = this._createObtainStreamMethod();
-
- if (!this.obtainStream) {
- logger.info('Desktop sharing disabled');
- }
- },
-
- /**
- * Returns a method which will be used to obtain the screen sharing stream
- * (based on the browser type).
- *
- * @returns {Function}
- * @private
- */
- _createObtainStreamMethod() {
- if (browser.isNWJS()) {
- return (onSuccess, onFailure) => {
- window.JitsiMeetNW.obtainDesktopStream(
- onSuccess,
- (error, constraints) => {
- let jitsiError;
-
- // FIXME:
- // This is very very dirty fix for recognising that the
- // user have clicked the cancel button from the Desktop
- // sharing pick window. The proper solution would be to
- // detect this in the NWJS application by checking the
- // streamId === "". Even better solution would be to
- // stop calling GUM from the NWJS app and just pass the
- // streamId to lib-jitsi-meet. This way the desktop
- // sharing implementation for NWJS and chrome extension
- // will be the same and lib-jitsi-meet will be able to
- // control the constraints, check the streamId, etc.
- //
- // I cannot find documentation about "InvalidStateError"
- // but this is what we are receiving from GUM when the
- // streamId for the desktop sharing is "".
-
- if (error && error.name === 'InvalidStateError') {
- jitsiError = new JitsiTrackError(
- JitsiTrackErrors.SCREENSHARING_USER_CANCELED
- );
- } else {
- jitsiError = new JitsiTrackError(
- error, constraints, [ 'desktop' ]);
- }
- (typeof onFailure === 'function')
- && onFailure(jitsiError);
- });
- };
- } else if (browser.isElectron()) {
- return this.obtainScreenOnElectron;
- } else if (browser.isReactNative() && browser.supportsGetDisplayMedia()) {
- return this.obtainScreenFromGetDisplayMediaRN;
- } else if (browser.supportsGetDisplayMedia()) {
- return this.obtainScreenFromGetDisplayMedia;
- }
- logger.log('Screen sharing not supported on ', browser.getName());
-
- return null;
- },
-
- /**
- * Gets the appropriate constraints for audio sharing.
- *
- * @returns {Object|boolean}
- */
- _getAudioConstraints() {
- const { audioQuality } = this.options;
- const audio = audioQuality?.stereo ? {
- autoGainControl: false,
- channelCount: 2,
- echoCancellation: false,
- noiseSuppression: false
- } : true;
-
- return audio;
- },
-
- /**
- * Checks whether obtaining a screen capture is supported in the current
- * environment.
- * @returns {boolean}
- */
- isSupported() {
- return this.obtainStream !== null;
- },
-
- /**
- * Obtains a screen capture stream on Electron.
- *
- * @param onSuccess - Success callback.
- * @param onFailure - Failure callback.
- */
- obtainScreenOnElectron(onSuccess, onFailure) {
- if (window.JitsiMeetScreenObtainer && window.JitsiMeetScreenObtainer.openDesktopPicker) {
- const { desktopSharingFrameRate, desktopSharingSources } = this.options;
-
- window.JitsiMeetScreenObtainer.openDesktopPicker(
- {
- desktopSharingSources: desktopSharingSources || [ 'screen', 'window' ]
- },
- (streamId, streamType, screenShareAudio = false) => {
- if (streamId) {
- let audioConstraints = false;
-
- if (screenShareAudio) {
- audioConstraints = {};
- const optionalConstraints = this._getAudioConstraints();
-
- if (typeof optionalConstraints !== 'boolean') {
- audioConstraints = {
- optional: optionalConstraints
- };
- }
-
- // Audio screen sharing for electron only works for screen type devices.
- // i.e. when the user shares the whole desktop.
- // Note. The documentation specifies that chromeMediaSourceId should not be present
- // which, in the case a users has multiple monitors, leads to them being shared all
- // at once. However we tested with chromeMediaSourceId present and it seems to be
- // working properly.
- if (streamType === 'screen') {
- audioConstraints.mandatory = {
- chromeMediaSource: 'desktop'
- };
- }
- }
-
- const constraints = {
- audio: audioConstraints,
- video: {
- mandatory: {
- chromeMediaSource: 'desktop',
- chromeMediaSourceId: streamId,
- minFrameRate: desktopSharingFrameRate?.min ?? SS_DEFAULT_FRAME_RATE,
- maxFrameRate: desktopSharingFrameRate?.max ?? SS_DEFAULT_FRAME_RATE,
- maxWidth: window.screen.width,
- maxHeight: window.screen.height
- }
- }
- };
-
- // We have to use the old API on Electron to get a desktop stream.
- navigator.mediaDevices.getUserMedia(constraints)
- .then(stream => onSuccess({
- stream,
- sourceId: streamId,
- sourceType: streamType
- }), onFailure);
- } else {
- // As noted in Chrome Desktop Capture API:
- // If user didn't select any source (i.e. canceled the prompt)
- // then the callback is called with an empty streamId.
- onFailure(new JitsiTrackError(JitsiTrackErrors.SCREENSHARING_USER_CANCELED));
- }
- },
- err => onFailure(new JitsiTrackError(
- JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_ERROR,
- err
- ))
- );
- } else {
- onFailure(new JitsiTrackError(JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_NOT_FOUND));
- }
- },
-
- /**
- * Obtains a screen capture stream using getDisplayMedia.
- *
- * @param callback - The success callback.
- * @param errorCallback - The error callback.
- */
- obtainScreenFromGetDisplayMedia(callback, errorCallback) {
- let getDisplayMedia;
-
- if (navigator.getDisplayMedia) {
- getDisplayMedia = navigator.getDisplayMedia.bind(navigator);
- } else {
- // eslint-disable-next-line max-len
- getDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices);
- }
-
- const { desktopSharingFrameRate } = this.options;
- const video = typeof desktopSharingFrameRate === 'object' ? { frameRate: desktopSharingFrameRate } : true;
- const audio = this._getAudioConstraints();
-
- // At the time of this writing 'min' constraint for fps is not supported by getDisplayMedia.
- video.frameRate && delete video.frameRate.min;
-
- const constraints = {
- video,
- audio,
- cursor: 'always'
- };
-
- logger.info('Using getDisplayMedia for screen sharing', constraints);
-
- getDisplayMedia(constraints)
- .then(stream => {
- callback({
- stream,
- sourceId: stream.id
- });
- })
- .catch(error => {
- const errorDetails = {
- errorName: error && error.name,
- errorMsg: error && error.message,
- errorStack: error && error.stack
- };
-
- logger.error('getDisplayMedia error', constraints, errorDetails);
-
- if (errorDetails.errorMsg && errorDetails.errorMsg.indexOf('denied by system') !== -1) {
- // On Chrome this is the only thing different between error returned when user cancels
- // and when no permission was given on the OS level.
- errorCallback(new JitsiTrackError(JitsiTrackErrors.PERMISSION_DENIED));
-
- return;
- }
-
- errorCallback(new JitsiTrackError(JitsiTrackErrors.SCREENSHARING_USER_CANCELED));
- });
- },
-
- /**
- * Obtains a screen capture stream using getDisplayMedia.
- *
- * @param callback - The success callback.
- * @param errorCallback - The error callback.
- */
- obtainScreenFromGetDisplayMediaRN(callback, errorCallback) {
- logger.info('Using getDisplayMedia for screen sharing');
-
- navigator.mediaDevices.getDisplayMedia({ video: true })
- .then(stream => {
- callback({
- stream,
- sourceId: stream.id });
- })
- .catch(() => {
- errorCallback(new JitsiTrackError(JitsiTrackErrors
- .SCREENSHARING_USER_CANCELED));
- });
- },
-
- /**
- * Sets the max frame rate to be used for a desktop track capture.
- *
- * @param {number} maxFps capture frame rate to be used for desktop tracks.
- * @returns {void}
- */
- setDesktopSharingFrameRate(maxFps) {
- logger.info(`Setting the desktop capture rate to ${maxFps}`);
-
- this.options.desktopSharingFrameRate = {
- min: SS_DEFAULT_FRAME_RATE,
- max: maxFps
- };
- }
- };
-
- export default ScreenObtainer;
|