13

I'm developing an application for iOS, Android and Windows with React Native in which I need to show a webpage through a WebView. That webpage accesses the camera of the device, so it uses MediaDevices.getUserMedia() Javascript function.

It works without problems in desktop, even in the Chrome app on the smartphone. However, when I call the webpage via the React Native <WebView />, I get a PermissionDenied error. The problem is that no request is shown to let me accept that permission. It justs denies the request without asking.

Here is a sample of my WebView element:

    <WebView 
        style={{flex: 1}}
        mediaPlaybackRequiresUserAction={false}
        domStorageEnabled={true}
        allowsInlineMediaPlayback={true}
        source={{uri: 'https://myurltomyapp.com/index.html'}} 
        startInLoadingState={true}
        allowUniversalAccessFromFileURLs={true}
    />

I've got all necessary permissions set on AndroidManifest.xml (and even some not needed for the sake of testing):

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
    <uses-permission android:name="android.permission.CAPTURE_SECURE_VIDEO_OUTPUT" />
    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.RECORD_VIDEO"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

And yes, the URL where is hosted the website is SSL certified, so it goes through HTTPS. It works on Chrome app, so it should work on WebView, too.

I've been reading through the net, and it seems that's because React Native WebView lacks of a onPermissionRequest() implementation, so it fails silently (and denies it).

Is there a way to implement it overriding the native WebView in React Native? Which files do I have to edit, if possible? Is there any third-party module (up to date) which has this implemented (haven't been able to find any)?

Thank you.

Unapedra
  • 2,043
  • 4
  • 25
  • 42

4 Answers4

15

Finally I had to implement an own WebView component in native Android. The problem is the onPermissionRequest() function that it's not implemented in the native WebView. You have to create your own and override that function. In my case, I had to add the following code (all files are under path [react-native-project]/android/app/src/main/java/com/listproject/permissionwebview/):

In PermissionWebviewPackage.java:

package com.listproject.permissionwebview;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;

/**
 * Add this package to getPackages() method in MainActivity.
 */

public class PermissionWebviewPackage implements ReactPackage {

    @Override
    public List<ViewManager> createViewManagers(
            ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
            new PermissionWebviewViewManager()
          );
    }

    @Override
    public List<NativeModule> createNativeModules(
            ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

In PermissionWebviewView.java:

package com.listproject.permissionwebview;
import android.content.Context;
import android.widget.LinearLayout;
import android.os.Bundle;
import android.widget.Toast;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.PermissionRequest;
import android.webkit.WebSettings;
import android.net.http.SslCertificate;
import android.net.http.SslError;
import android.webkit.SslErrorHandler;
import android.support.v4.app.ActivityCompat;
import android.app.LocalActivityManager;
import android.view.ViewGroup;
import android.Manifest;
import android.app.Activity;
import com.listproject.MainActivity;
import com.listproject.R;

public class PermissionWebviewView extends WebView{

    private Context context;

    public PermissionWebviewView(Context context) {
        super(context);
        this.context = context;

        this.setWebViewClient(new WebViewClient());

        WebSettings webSettings = this.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webSettings.setAllowFileAccessFromFileURLs(true);
        webSettings.setAllowUniversalAccessFromFileURLs(true);
        webSettings.setMediaPlaybackRequiresUserGesture(false);
        webSettings.setUseWideViewPort(true);
        webSettings.setDomStorageEnabled(true);

        this.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onPermissionRequest(final PermissionRequest request) {
                request.grant(request.getResources());
            }
        });
    }
}

In PermissionWebviewViewManager.java:

package com.listproject.permissionwebview;

import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;

public class PermissionWebviewViewManager extends SimpleViewManager<PermissionWebviewView> {

    public static final String REACT_CLASS = "PermissionWebviewViewManager";
    private String source;

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @Override
    public PermissionWebviewView createViewInstance(ThemedReactContext context) {
        return new PermissionWebviewView(context); //If your customview has more constructor parameters pass it from here.
    }

    @ReactProp(name = "sourceUri")
    public void setSource(PermissionWebviewView view, String source) {
        view.loadUrl(source);
    }
}

Finally, update your MainApplication.java file, and add your package into getPackages() function:

@Override
protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(
      new MainReactPackage(),
        [...],
        new PermissionWebviewPackage()
  );
}

Keep in mind that the names of listproject, permissionwebview, and so on can be changed to whatever you need to, as long as you change them in the namespaces of the files and in the package references.

Once you have all this, all you have to create is the ReactNative component export ([react-native-project]/app/components/PermissionWebView/index.android.js):

import PropTypes from 'prop-types';
import {requireNativeComponent, ViewPropTypes} from 'react-native';

// The 'name' property is not important, the important one is below
var mcs = {
  name: 'PermissionWebview',
  propTypes: {
    sourceUri: PropTypes.string,
      ...ViewPropTypes
  }
};

module.exports = requireNativeComponent('PermissionWebviewViewManager', mcs);

Note we set the name of index to index.android.js because this component is made for Android, so it's included only in Android platform.

With this, everything should work and the WebView component should ask for permission when it's used.

EDIT:

I've uploaded the code to my Github repository. I've only changed some names, but the core code has not changed a bit to make sure it still works (I cannot test it, so better not to change lots of things).

Hope it helps someone!

Unapedra
  • 2,043
  • 4
  • 25
  • 42
  • Possible for you to share working sample from github or somewhere ? I am facing difficulties in porting this code – Jaffer Sathick Mar 06 '18 at 07:23
  • Did you get a solution for getUserMedia on iOS? – AndrewSteinheiser Feb 08 '19 at 08:27
  • I'm afraid that no, sorry. However, at this time, React Native maybe has changed so much that not even the Android code is valid anymore. Maybe they've already found a quickaround for this! – Unapedra Feb 11 '19 at 11:14
  • This works as expected. But the `injectedJavaScript` prop is not working. – Dhruv Marwha May 13 '19 at 07:34
  • Please post solution for ios too. – shruti garg Jun 10 '19 at 06:27
  • Your github code has enabled permissions in webview but still camera is not opening – shruti garg Jun 10 '19 at 12:22
  • @shrutigarg keep in mind that this code is more than 1 year old, so things may have changed since then. I'm not working in this project anymore, so I cannot assure it's still working, I'm sorry. I remember I also had to give permission to the device (the device asks you if you want to grant camera access to the application). You should grant it, and it should work if everything is as it was. – Unapedra Jun 11 '19 at 07:23
  • Just a quick question, does this access the frontal camera of the phone or the back-facing camera? – Viktor Nov 05 '20 at 10:32
2

In my case the following code works.

Step 1: Add permissions to AndroidManifest.xml

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MICROPHONE" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- any additional permissions -->

Step 2: Add import statement for 'PermissionsAndroid' from 'react-native' package

import {
  PermissionsAndroid
} from 'react-native';

Step 3: Write async methods to acquire required permission, here I am getting Camera and Microphone permissions

cameraPermission = async () => {

    let granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.CAMERA,
      {
        title: "Camera Permission",
        message:
          "App needs access to your camera " +
          "so others can see you.",
        buttonNeutral: "Ask Me Later",
        buttonNegative: "Cancel",
        buttonPositive: "OK"
      }
    );
    if (granted === PermissionsAndroid.RESULTS.GRANTED) {
      console.log("You can use the camera");
    } else {
      console.log("Camera permission denied");
    }
}

micPermission = async () => {

let granted = await PermissionsAndroid.request(
  PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
  {
    title: "Audio Permission",
    message:
      "App needs access to your audio / microphone",
    buttonNeutral: "Ask Me Later",
    buttonNegative: "Cancel",
    buttonPositive: "OK"
  }
);

if (granted === PermissionsAndroid.RESULTS.GRANTED) {
  console.log("You can use the Microphone");
} else {
  console.log("Microphone permission denied");
}  

micPermission = async () => {
    let granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
      {
        title: "Audio Permission",
        message:
          "App needs access to your audio / microphone",
        buttonNeutral: "Ask Me Later",
        buttonNegative: "Cancel",
        buttonPositive: "OK"
      }
    );

    if (granted === PermissionsAndroid.RESULTS.GRANTED) {
      console.log("You can use the Microphone");
    } else {
      console.log("Microphone permission denied");
    }    
}

Step 4: Trigger the permission on componentDidMount

componentDidMount() {
  this.cameraPermission();
  this.micPermission();
}

Step 5: Add WebView with media attributes

constructor(props) {
    super(props);
    this.state = {
      url: 'https://yoururl.com',
      userAgent: 'Mozilla/5.0 (Linux; An33qdroid 10; Android SDK built for x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.185 Mobile Safari/537.36'
    }
  }
render() {
return (
  <WebView
    userAgent={this.state.userAgent} //Set your useragent (Browser) **Very Important
    originWhitelist={['*']}
    allowsInlineMediaPlayback
    bounces={true}
    source={{
      uri: this.state.url, //URL of your redirect site
    }}
    startInLoadingState
    scalesPageToFit
    javaScriptEnabled={true}
  />
);
}

Hope this is helpful!

Adheep
  • 1,585
  • 1
  • 14
  • 27
  • 1
    what's the this.state.userAgent set to? – Ilia Sidorenko Apr 27 '21 at 22:13
  • I tried this and still facing the same issue. How to get a userAgent? – Daryl Aranha Jun 27 '21 at 05:37
  • @DarylAranha and @ilia-sidorenko, I have updated the answer. `userAgent: 'Mozilla/5.0 (Linux; An33qdroid 10; Android SDK built for x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.185 Mobile Safari/537.36'` – Adheep Jul 13 '21 at 06:07
  • @AdheepMohamedAbdulKader Is this code still works for you? As I am facing the same issue with WebView https://stackoverflow.com/questions/69812167/error-opening-your-camera-and-or-microphone-could-not-start-audio-source-in-web – Harsh Mishra Nov 03 '21 at 07:32
  • @DarylAranha Have you solved this issue in your project? – Harsh Mishra Nov 03 '21 at 07:33
1

I think onPermissionRequest() method is duplicated in the latest version of react native webview package. I just commented the second one resolved my issue.

You should comment on the RNCWebViewManager.java file. You can get it from the following path node_modules\react-native-webview\android\src\main\java\com\reactnativecommunity\webview

enter image description here

enter image description here

Codemaker2015
  • 12,190
  • 6
  • 97
  • 81
  • I can't find any function name `onPermissionRequest()` in the updated webview package. Can you please send the whole code maybe it is missing that's why I am facing the same issue? – Harsh Mishra Nov 03 '21 at 07:35
0

This isn't possibly locally.

I had issues while I tried loading an injected HTML and imported third-party JavaScript file into the WebView (Through unpkg).

navigator.getUserMedia is undefined when not served via HTTPS or localhost.

When developing in React Native, you won't always have access to localhost (especially if you're developing via WiFi using your phone).

Therefore, a solution is to host the content on a remote backend and serve it through https.

Jose A
  • 10,053
  • 11
  • 75
  • 108