> For the complete documentation index, see [llms.txt](https://docs.vida.id/identity-stack/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.vida.id/identity-stack/verify/liveness/integration/liveness-web-sdk/webview-camera-prerequisites.md).

# WebView Camera Prerequisites

This page is for client mobile app developers who embed the VIDA Liveness Web SDK - or a VIDA-hosted verification page — inside an in-app **WebView**. It explains why the camera may fail to load in a WebView and how to make the app work.

## Overview

The SDK uses the standard browser API `navigator.mediaDevices.getUserMedia()` to access the camera. Inside an in-app WebView, that call only succeeds when **all three** of the following are true:

1. The app has been granted OS camera permission.
2. The app forwards and grants the WebView's camera request in native code. **This is the step most often missing.**
3. The page is served over a secure context (HTTPS).

{% hint style="info" %}
If step 2 is not implemented, the camera fails silently — the SDK reports **error `70001` (camera permission denied)** even when the app itself already has camera permission.

This is a native integration requirement, not an SDK defect: the web layer cannot grant native or WebView permissions on its own.&#x20;
{% endhint %}

## Minimum WebView / Engine Version

Beyond permissions, the WebView engine has capability to run the SDK's face-detection engine (MediaPipe), which requires **WebGL2** and **WebAssembly**.

* **Android:** Android System WebView / Chromium **≥ 101**.
* **iOS:** the WebView uses the system WebKit, which stays current with the OS. **iOS 14.3+** is required for `getUserMedia` in `WKWebView`.

<table><thead><tr><th width="182.6015625">Android OS (launch)</th><th width="229.73046875">WebView shipped at launch</th><th>Updatable to ≥ 101?</th></tr></thead><tbody><tr><td>Android 8 / 9</td><td>~58–69</td><td>Limited (Chromium dropped support after v109)</td></tr><tr><td>Android 10</td><td>~77</td><td>Yes - update via Play Store</td></tr><tr><td>Android 11</td><td>~83–85</td><td>Yes</td></tr><tr><td>Android 12+</td><td>~94+</td><td>Yes</td></tr></tbody></table>

### Why this matters

**Android System WebView updates separately from the OS.** It is a Play Store component, so its version is independent of the Android version — a device on Android 10 shipped with WebView \~77 (released 2019) and stays there until someone updates it.

When the WebView is too old, the failure is easy to misread:

* It cannot load WebGL2 / WASM, and may not reach the SDK's model/asset CDN.
* The flow gets **stuck on the loading spinner** — the camera never opens.
* No telemetry is captured for the session, so support sees an **empty log**.

The two failure modes look different, which lets you tell them apart at a glance:

| What the user sees                  | Likely cause                                                                                                                                                                                                                                  | Signal                    |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| Immediate error, no loading         | Permission problem (see the [three layers](https://app.gitbook.com/o/HFo4Mq4hPbfFvhTREpfY/s/C2s0IISjZwzGfw8dm215/~/edit/~/changes/178/verify/liveness/integration/liveness-web-sdk/webview-camera-prerequisites#the-three-permission-layers)) | Error `70001`             |
| Loading spinner forever, then fails | Outdated WebView engine (< v101)                                                                                                                                                                                                              | No error code; empty logs |

### How to check and update it (device side)

1. Open the Play Store → search **"Android System WebView"** → **Update**. Also update Chrome, which provides the WebView engine on some devices.
2. Or verify the installed version: ***Settings → Apps → Android System WebView***.
3. After updating, fully close and reopen the host app, then retry the flow.

### The three permission layers

Granting permission at one layer does **not** propagate to the others.

<table><thead><tr><th width="201.07421875">Layer</th><th width="209.7578125">Controlled by</th><th>How it's granted</th></tr></thead><tbody><tr><td><strong>OS / app permission</strong></td><td>Android / iOS</td><td>User grants the host app camera access (runtime prompt / Settings).</td></tr><tr><td><strong>WebView delegation</strong></td><td>Your native app code</td><td>The app must explicitly handle and grant the WebView's camera permission request. There is no default behavior on Android.</td></tr><tr><td><strong>Origin / site permission</strong></td><td>The WebView engine</td><td>Result of <code>getUserMedia()</code> after layer 2 allows it.</td></tr></tbody></table>

{% hint style="info" %}
In a normal mobile browser, layers 2 and 3 are handled for you. In an embedded WebView, **layer 2 is your responsibility**. Without it, `getUserMedia()` throws `NotAllowedError` → SDK error `70001`.
{% endhint %}

### Android (`android.webkit.WebView`)

#### 1. Declare and request the OS camera permission

In `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
```

Request the runtime permission **before** loading the page (Android 6+):

```kotlin
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), REQ_CAMERA)
}
```

#### 2. Grant the WebView's permission request (the critical step)

Set a `WebChromeClient` and handle `onPermissionRequest`. Without this, the WebView denies the camera by default:

```kotlin
webView.webChromeClient = object : WebChromeClient() {
    override fun onPermissionRequest(request: PermissionRequest) {
        val needsCamera = request.resources.any {
            it == PermissionRequest.RESOURCE_VIDEO_CAPTURE
        }
        if (needsCamera &&
            ContextCompat.checkSelfPermission(this@MyActivity, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            // Grant only what was requested (camera; the SDK does not request audio)
            request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
        } else {
            request.deny()
            // optionally trigger the OS runtime prompt, then reload the page
        }
    }
}
```

#### 3. Enable the required WebView settings

```kotlin
webView.settings.apply {
    javaScriptEnabled = true
    mediaPlaybackRequiresUserGesture = false  // allow camera/stream without an extra tap
    domStorageEnabled = true                  // SDK uses localStorage
}
```

#### 4. Serve over HTTPS

`getUserMedia` requires a **secure context**. Load the SDK/hosted page over `https://` (not `http://`, and not a `file://` page making cross-origin calls). Do not disable TLS validation.

#### Common Android pitfalls

* No **`WebChromeClient.onPermissionRequest`** override → camera always denied. This is the most common cause.
* Granting the WebView request **before** the app has the OS permission → still denied. Request OS permission first.
* `mediaPlaybackRequiresUserGesture = true` (the default) can block the stream from starting.
* Using a heavily customized or outdated WebView (System WebView not updated) that lacks WebRTC support.

### iOS (`WKWebView`)

{% hint style="info" %}
Use `WKWebView`. The legacy `UIWebView` does not support `getUserMedia` at all.
{% endhint %}

#### 1. Declare the usage description

In `Info.plist`:

```xml
<key>NSCameraUsageDescription</key>
<string>We need camera access to verify your identity.</string>
```

The OS prompts the user the first time the camera is accessed. Without this key, the app crashes on access.

#### 2. Configure WKWebView for inline media capture

```swift
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []  // don't require a gesture to start the stream
let webView = WKWebView(frame: .zero, configuration: config)
```

#### 3. (iOS 15+) Auto-grant the WebView capture prompt if desired

On iOS 15+ you can implement `WKUIDelegate` to control the per-page capture permission instead of showing the WebView's own prompt:

```swift
webView.uiDelegate = self

@available(iOS 15.0, *)
func webView(_ webView: WKWebView,
             requestMediaCapturePermissionFor origin: WKSecurityOrigin,
             initiatedByFrame frame: WKFrameInfo,
             type: WKMediaCaptureType,
             decisionHandler: @escaping (WKPermissionDecision) -> Void) {
    decisionHandler(.grant)  // or .prompt / .deny per your policy
}
```

#### 4. Serve over HTTPS

Same secure-context requirement as Android. Avoid ATS exceptions that downgrade security.

#### Common iOS pitfalls

* Using `UIWebView` instead of `WKWebView` → camera never works.
* Missing `NSCameraUsageDescription` → app crashes when the camera is accessed.
* `allowsInlineMediaPlayback = false`, or requiring a user gesture → the stream won't start.
* The user previously denied OS camera permission to the app → it must be re-enabled in ***Settings → \[App] → Camera***. The WebView cannot override this.

## Full sample code

Complete, drop-in samples with all required settings applied. Replace the placeholder URL with the VIDA flow / hosted link you were given.

### 1. Android

<details>

<summary>Android (MainActivity.java)</summary>

{% code expandable="true" %}

```java
package id.vida.webekyc;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.webkit.PermissionRequest;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private static final int REQ_CAMERA = 100;
    private static final String VIDA_URL = "<load the url here>";

    WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWebView = findViewById(R.id.webview);

        // Grant the WebView's camera request (the critical step).
        mWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onPermissionRequest(final PermissionRequest request) {
                // Grant only the camera capture resource the SDK asks for (no audio).
                for (String resource : request.getResources()) {
                    if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource)) {
                        request.grant(new String[]{PermissionRequest.RESOURCE_VIDEO_CAPTURE});
                        return;
                    }
                }
                request.deny();
            }
        });

        WebSettings webSettings = mWebView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webSettings.setDomStorageEnabled(true);                     // SDK uses localStorage
        webSettings.setMediaPlaybackRequiresUserGesture(false);     // REQUIRED: let the camera stream start

        mWebView.setWebViewClient(new WebViewClient());

        // Camera permission is needed to capture the image.
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            mWebView.loadUrl(VIDA_URL);
        } else {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQ_CAMERA);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQ_CAMERA) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                mWebView.loadUrl(VIDA_URL);
            } else {
                Toast.makeText(this, "Camera permission is required to continue.", Toast.LENGTH_SHORT).show();
                // Show your error screen — the flow cannot continue without camera permission.
            }
        }
    }
}
```

{% endcode %}

</details>

**Changes vs. the basic snippets:** added `setMediaPlaybackRequiresUserGesture(false)` (without it the camera stream can be blocked from starting), and `onPermissionRequest` now grants only `RESOURCE_VIDEO_CAPTURE` instead of every requested resource.

`AndroidManifest.xml` must also declare:

```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
```

### 2. iOS

<details>

<summary>iOS (ViewController.swift)</summary>

{% code expandable="true" %}

```swift
import UIKit
import WebKit

class ViewController: UIViewController, WKUIDelegate {

    var webView: WKWebView!

    override func loadView() {
        let webConfiguration = WKWebViewConfiguration()

        // Allow inline (non-fullscreen) camera capture.
        webConfiguration.allowsInlineMediaPlayback = true
        // REQUIRED: do not require a user gesture to start the camera stream.
        webConfiguration.mediaTypesRequiringUserActionForPlayback = []

        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.uiDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        let url = URL(string: "<#Add the URL shared#>")!
        webView.load(URLRequest(url: url))
    }

    // iOS 15+: auto-grant the WebView's camera capture prompt.
    @available(iOS 15.0, *)
    func webView(_ webView: WKWebView,
                 requestMediaCapturePermissionFor origin: WKSecurityOrigin,
                 initiatedByFrame frame: WKFrameInfo,
                 type: WKMediaCaptureType,
                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        decisionHandler(.grant)  // or .prompt / .deny per your policy
    }
}
```

{% endcode %}

</details>

**Changes vs. the basic snippets:** added `mediaTypesRequiringUserActionForPlayback = []` (the iOS equivalent of Android's media-gesture setting) and the iOS 15+ capture-permission delegate.

`Info.plist` must also declare:

```xml
<key>NSCameraUsageDescription</key>
<string>We need camera access to verify your identity.</string>
```

### 3. React Native

Use the `react-native-webview` package (v11+). The same three layers apply — the JS props below cover the WebView delegation and settings, but you still need the **native OS permission** and the **manifest / Info.plist** entries.

| Prop                                      | Purpose                                                                |
| ----------------------------------------- | ---------------------------------------------------------------------- |
| `mediaPlaybackRequiresUserAction={false}` | Allow the camera stream to start without an extra tap (Android + iOS). |
| `allowsInlineMediaPlayback`               | Inline (non-fullscreen) capture on iOS.                                |
| `mediaCapturePermissionGrantType="grant"` | Auto-grant the `getUserMedia` prompt on iOS 15+.                       |
| `javaScriptEnabled`, `domStorageEnabled`  | JS + `localStorage` (SDK requirement).                                 |

{% hint style="info" %}
On **Android**, `react-native-webview` automatically grants the WebView's camera request **only if the app already holds the OS `CAMERA` permission** — so you must request it *before* the WebView mounts.
{% endhint %}

<details>

<summary><strong>Full sample (<code>VidaWebView.jsx</code>)</strong></summary>

{% code expandable="true" %}

```jsx
import React, { useEffect, useState } from 'react';
import { Platform, PermissionsAndroid, SafeAreaView, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';

const VIDA_URL = '<load the url here>';

export default function VidaWebView() {
  // iOS asks for camera permission via the WebView itself; Android needs an explicit request first.
  const [ready, setReady] = useState(Platform.OS === 'ios');

  useEffect(() => {
    if (Platform.OS === 'android') {
      PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA).then((result) => {
        setReady(result === PermissionsAndroid.RESULTS.GRANTED);
        // If not granted, show your own error screen — the flow cannot continue.
      });
    }
  }, []);

  if (!ready) return <ActivityIndicator style={{ flex: 1 }} />;

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <WebView
        source={{ uri: VIDA_URL }}
        originWhitelist={['https://*']}
        javaScriptEnabled
        domStorageEnabled
        allowsInlineMediaPlayback
        mediaPlaybackRequiresUserAction={false}
        mediaCapturePermissionGrantType="grant"
      />
    </SafeAreaView>
  );
}
```

{% endcode %}

</details>

You must **also** add the native entries:

* **Android** `android/app/src/main/AndroidManifest.xml`: `<uses-permission android:name="android.permission.CAMERA" />`
* **iOS** `ios/<App>/Info.plist`: `NSCameraUsageDescription` (a string explaining camera use).

### 4. Flutter (`flutter_inappwebview`)

For camera / `getUserMedia` inside a Flutter WebView, use `flutter_inappwebview` (v6+) — it exposes the `onPermissionRequest` callback and media settings needed for the camera. (The official `webview_flutter` works too; see the note below.) Pair it with `permission_handler` for the OS-level prompt.

| Setting / callback                        | Purpose                                                         |
| ----------------------------------------- | --------------------------------------------------------------- |
| `mediaPlaybackRequiresUserGesture: false` | Allow the camera stream to start without a tap (Android + iOS). |
| `allowsInlineMediaPlayback: true`         | Inline capture on iOS.                                          |
| `onPermissionRequest → GRANT`             | Grant the WebView's camera request (Android + iOS 15+).         |
| `iframeAllow: "camera; microphone"`       | Needed if the SDK runs inside an iframe on the page.            |

<details>

<summary><strong>Full sample (<code>vida_webview.dart</code>)</strong></summary>

{% code expandable="true" %}

```dart
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:permission_handler/permission_handler.dart';

class VidaWebView extends StatefulWidget {
  const VidaWebView({super.key});

  @override
  State<VidaWebView> createState() => _VidaWebViewState();
}

class _VidaWebViewState extends State<VidaWebView> {
  static const String vidaUrl = '<load the url here>';

  @override
  void initState() {
    super.initState();
    Permission.camera.request(); // request the OS camera permission up front
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: InAppWebView(
          initialUrlRequest: URLRequest(url: WebUri(vidaUrl)),
          initialSettings: InAppWebViewSettings(
            javaScriptEnabled: true,
            mediaPlaybackRequiresUserGesture: false, // REQUIRED: allow the camera stream to start
            allowsInlineMediaPlayback: true,         // iOS inline capture
            iframeAllow: 'camera; microphone',       // if the SDK is embedded in an iframe
            iframeAllowFullscreen: true,
          ),
          onPermissionRequest: (controller, request) async {
            // Grant the WebView's camera request (Android + iOS 15+).
            return PermissionResponse(
              resources: request.resources,
              action: PermissionResponseAction.GRANT,
            );
          },
        ),
      ),
    );
  }
}
```

{% endcode %}

</details>

You must **also** add the native entries:

* **Android** `android/app/src/main/AndroidManifest.xml`: `<uses-permission android:name="android.permission.CAMERA" />`
* **iOS** `ios/Runner/Info.plist`: `NSCameraUsageDescription` (a string explaining camera use).

**Using the official `webview_flutter` instead?** On Android you must grant the request explicitly:

```dart
if (controller.platform is AndroidWebViewController) {
  (controller.platform as AndroidWebViewController)
    ..setMediaPlaybackRequiresUserGesture(false)
    ..setOnPlatformPermissionRequest((req) => req.grant());
}
```

And create the iOS controller with `allowsInlineMediaPlayback: true` + `mediaTypesRequiringUserAction: {}`.

### How to tell which layer is failing

| Symptom                                                                                                   | Likely layer                                                                     | Fix                                                                                                                                                                                          |
| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| SDK shows error `70001` immediately; no OS prompt ever appeared                                           | Layer 2 (Android `onPermissionRequest` missing / iOS delegate denying)           | Implement the native permission handler.                                                                                                                                                     |
| OS camera prompt appeared, user allowed, still fails                                                      | Layer 2 or secure context                                                        | Verify `grant()` is called for `RESOURCE_VIDEO_CAPTURE`; confirm HTTPS.                                                                                                                      |
| Stuck on the loading spinner; camera preview never appears, then fails. No logs captured for the session. | Outdated WebView engine (< v101) — can't load WebGL2/WASM or reach the model CDN | Update Android System WebView to ≥ 101 via Play Store (see [Minimum WebView / engine version](https://claude.ai/chat/ff48d53f-8d56-4913-8e6f-06f44e5b621c#minimum-webview--engine-version)). |
| Works in mobile Safari/Chrome but not in the app                                                          | WebView config                                                                   | Compare against the settings above (`WKWebView` / `WebChromeClient`).                                                                                                                        |
| Error `70003` (not supported) / `70005` (no camera)                                                       | Engine / device                                                                  | Old WebView without WebRTC, or no camera device.                                                                                                                                             |
| Worked once, now skips straight to a denied error                                                         | SDK caches an "allowed" flag in `localStorage` independent of real permission    | Clear WebView storage, or re-grant in OS settings then reload.                                                                                                                               |

{% hint style="info" %}
The SDK shows its own "Allow camera access" screen with an **Allow** button. Inside a WebView this is a **UI affordance only** — it does not grant any OS, WebView, or browser permission. It simply records that the user is ready, then triggers the real `getUserMedia()` call.

The actual permission outcome is determined entirely by the three layers above. A user tapping "Allow" inside the SDK will not change device or WebView permission settings; the native prerequisites in this document must still be met.
{% endhint %}
