[RN] Handle denied getUserMedia permissions

This commit is contained in:
Lyubo Marinov 2017-07-18 16:41:39 -05:00
parent c97daff506
commit a690b9d5e1
4 changed files with 145 additions and 82 deletions

View File

@ -43,6 +43,7 @@ import {
participantUpdated
} from './react/features/base/participants';
import {
createLocalTracks,
replaceLocalTrack,
trackAdded,
trackRemoved
@ -259,53 +260,6 @@ function assignWindowLocationPathname(pathname) {
windowLocation.pathname = pathname;
}
/**
* Create local tracks of specified types.
* @param {Object} options
* @param {string[]} options.devices - required track types
* ('audio', 'video' etc.)
* @param {string|null} (options.cameraDeviceId) - camera device id, if
* undefined - one from settings will be used
* @param {string|null} (options.micDeviceId) - microphone device id, if
* undefined - one from settings will be used
* @param {boolean} (checkForPermissionPrompt) - if lib-jitsi-meet should check
* for gUM permission prompt
* @returns {Promise<JitsiLocalTrack[]>}
*/
function createLocalTracks(options, checkForPermissionPrompt) {
options || (options = {});
return JitsiMeetJS
.createLocalTracks({
// copy array to avoid mutations inside library
devices: options.devices.slice(0),
desktopSharingSources: options.desktopSharingSources,
resolution: config.resolution,
cameraDeviceId: typeof options.cameraDeviceId === 'undefined' ||
options.cameraDeviceId === null
? APP.settings.getCameraDeviceId()
: options.cameraDeviceId,
micDeviceId: typeof options.micDeviceId === 'undefined' ||
options.micDeviceId === null
? APP.settings.getMicDeviceId()
: options.micDeviceId,
// adds any ff fake device settings if any
firefox_fake_device: config.firefox_fake_device,
desktopSharingExtensionExternalInstallation:
options.desktopSharingExtensionExternalInstallation
}, checkForPermissionPrompt).then( (tracks) => {
tracks.forEach((track) => {
track.on(TrackEvents.NO_DATA_FROM_SOURCE,
APP.UI.showTrackNotWorkingDialog.bind(null, track));
});
return tracks;
}).catch(function (err) {
logger.error(
'failed to create local tracks', options.devices, err);
return Promise.reject(err);
});
}
class ConferenceConnector {
constructor(resolve, reject) {
this._resolve = resolve;

View File

@ -10,11 +10,8 @@ import {
} from '../media';
import { getLocalParticipant } from '../participants';
import {
TRACK_ADDED,
TRACK_REMOVED,
TRACK_UPDATED
} from './actionTypes';
import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from './actionTypes';
import { createLocalTracks } from './functions';
/**
* Request to start capturing local audio and/or video. By default, the user
@ -23,19 +20,45 @@ import {
* @param {Object} [options] - For info @see JitsiMeetJS.createLocalTracks.
* @returns {Function}
*/
export function createLocalTracks(options = {}) {
return dispatch =>
JitsiMeetJS.createLocalTracks({
cameraDeviceId: options.cameraDeviceId,
devices: options.devices || [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ],
facingMode: options.facingMode || CAMERA_FACING_MODE.USER,
micDeviceId: options.micDeviceId
})
.then(localTracks => dispatch(_updateLocalTracks(localTracks)))
.catch(err => {
console.error(
`JitsiMeetJS.createLocalTracks.catch rejection reason: ${err}`);
});
export function createInitialLocalTracks(options = {}) {
return (dispatch, getState) => {
const devices
= options.devices || [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ];
const store = {
dispatch,
getState
};
// The following executes on React Native only at the time of this
// writing. The effort to port Web's createInitialLocalTracksAndConnect
// is significant and that's where the function createLocalTracks got
// born. I started with the idea a porting so that we could inherit the
// ability to getUserMedia for audio only or video only if getUserMedia
// for audio and video fails. Eventually though, I realized that on
// mobile we do not have combined permission prompts implemented anyway
// (either because there are no such prompts or it does not make sense
// to implement them) and the right thing to do is to ask for each
// device separately.
for (const device of devices) {
createLocalTracks(
{
cameraDeviceId: options.cameraDeviceId,
devices: [ device ],
facingMode: options.facingMode || CAMERA_FACING_MODE.USER,
micDeviceId: options.micDeviceId
},
/* firePermissionPromptIsShownEvent */ false,
store)
.then(localTracks => dispatch(_updateLocalTracks(localTracks)));
// TODO The function createLocalTracks logs the rejection reason of
// JitsiMeetJS.createLocalTracks so there is no real benefit to
// logging it here as well. Technically though,
// _updateLocalTracks may cause a rejection so it may be nice to log
// it. It's not too big of a concern at the time of this writing
// because React Native warns on unhandled Promise rejections.
}
};
}
/**

View File

@ -1,5 +1,90 @@
/* global APP */
import JitsiMeetJS, { JitsiTrackEvents } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Create local tracks of specific types.
*
* @param {Object} options - The options with which the local tracks are to be
* created.
* @param {string|null} [options.cameraDeviceId] - Camera device id or
* {@code undefined} to use app's settings.
* @param {string[]} options.devices - Required track types such as 'audio'
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event.
* @param {Object} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}
*/
export function createLocalTracks(
options,
firePermissionPromptIsShownEvent,
store) {
options || (options = {}); // eslint-disable-line no-param-reassign
let { cameraDeviceId, micDeviceId } = options;
if (typeof APP !== 'undefined') {
// TODO The app's settings should go in the redux store and then the
// reliance on the global variable APP will go away.
if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
cameraDeviceId = APP.settings.getCameraDeviceId();
}
if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
micDeviceId = APP.settings.getMicDeviceId();
}
store || (store = APP.store); // eslint-disable-line no-param-reassign
}
const {
firefox_fake_device, // eslint-disable-line camelcase
resolution
} = store.getState()['features/base/config'];
return (
JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
desktopSharingExtensionExternalInstallation:
options.desktopSharingExtensionExternalInstallation,
desktopSharingSources: options.desktopSharingSources,
// Copy array to avoid mutations inside library.
devices: options.devices.slice(0),
firefox_fake_device, // eslint-disable-line camelcase
micDeviceId,
resolution
},
firePermissionPromptIsShownEvent)
.then(tracks => {
// TODO JitsiTrackEvents.NO_DATA_FROM_SOURCE should probably be
// dispatched in the redux store here and then
// APP.UI.showTrackNotWorkingDialog should be in a middleware
// somewhere else.
if (typeof APP !== 'undefined') {
tracks.forEach(track =>
track.on(
JitsiTrackEvents.NO_DATA_FROM_SOURCE,
APP.UI.showTrackNotWorkingDialog.bind(null, track)));
}
return tracks;
})
.catch(err => {
logger.error('Failed to create local tracks', options.devices, err);
return Promise.reject(err);
}));
}
/**
* Returns local audio track.
*

View File

@ -7,15 +7,15 @@ import {
SET_AUDIO_MUTED,
SET_CAMERA_FACING_MODE,
SET_VIDEO_MUTED,
TOGGLE_CAMERA_FACING_MODE,
setAudioMuted,
setVideoMuted
setVideoMuted,
TOGGLE_CAMERA_FACING_MODE,
toggleCameraFacingMode
} from '../media';
import { MiddlewareRegistry } from '../redux';
import {
_disposeAndRemoveTracks,
createLocalTracks,
createInitialLocalTracks,
destroyLocalTracks
} from './actions';
import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from './actionTypes';
@ -38,7 +38,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
case LIB_DID_INIT:
store.dispatch(createLocalTracks());
store.dispatch(createInitialLocalTracks());
break;
case SET_AUDIO_MUTED:
@ -46,21 +46,22 @@ MiddlewareRegistry.register(store => next => action => {
break;
case SET_CAMERA_FACING_MODE: {
// XXX Destroy the local video track before creating a new one or
// react-native-webrtc may be slow or get stuck when opening a (video)
// capturer twice.
// XXX The camera facing mode of a MediaStreamTrack can be specified
// only at initialization time and then it can only be toggled. So in
// order to set the camera facing mode, one may destroy the track and
// then initialize a new instance with the new camera facing mode. But
// that is inefficient on mobile at least so the following relies on the
// fact that there are 2 camera facing modes and merely toggles between
// them to (hopefully) get the camera in the specified state.
const localTrack = _getLocalTrack(store, MEDIA_TYPE.VIDEO);
let jitsiTrack;
if (localTrack) {
store.dispatch(_disposeAndRemoveTracks([ localTrack.jitsiTrack ]));
if (localTrack
&& (jitsiTrack = localTrack.jitsiTrack)
&& jitsiTrack.getCameraFacingMode()
!== action.cameraFacingMode) {
store.dispatch(toggleCameraFacingMode());
}
store.dispatch(
createLocalTracks({
devices: [ MEDIA_TYPE.VIDEO ],
facingMode: action.cameraFacingMode
})
);
break;
}