Embed bridge

Run the QRGen scanner in any WebView or iframe with the hosted embed page, its message bridge and host commands, or self-host it with qrgen-sdk/bridge.

Edit on GitHub

The embed page is a full-screen QRGen scanner that any app with a WebView can load: React Native, Flutter, native iOS and Android, .NET MAUI and WinUI, Titanium, Unity, or an <iframe> on another website. The page scans; your app receives the results as JSON messages and can send commands back. No QRGen code runs in your app.

https://rockyljewell.github.io/QR-GEN/embed/?symbologies=qr,ean13&mode=single

The page is served over HTTPS, so it can use the camera in any WebView that supports getUserMedia, as long as your app grants camera permission (see the host examples below). It loads the decoder from the same origin, so the WebView only needs access to rockyljewell.github.io. The React Native and Flutter packages wrap this for you.

Query options

The query string takes the scanner options. Lists are comma-separated, and booleans accept 1/0 or true/false.

Parameter Example Description
symbologies qr,ean13 Ids, aliases or groups. Default: all.
mode continuous single, continuous or batch. The hosted page defaults to single.
duplicateFilter 2000 Milliseconds; 0 every frame, -1 once per session.
beep, vibrate 0 Feedback on scan. Default on.
camera front back, front or a device id.
torch 1 Turn the torch on when supported.
viewfinder line frame, line or none.
scanArea 0.1,0.3,0.8,0.4 Normalized x,y,width,height.
maxResults 5 Codes per frame.
tryHarder 1 More effort per frame.
resolution fhd sd, hd, fhd or 4k.
accent %23ff6b35 Accent color. URL-encode # as %23.
hint Scan%20your%20ticket Instruction text. Empty hides it.
controls none Hide the built-in buttons.
autostart 0 Wait for a start command instead of opening the camera immediately.

Kebab-case names (duplicate-filter, scan-area, max-results, try-harder) work too. Add debug=1 to show an on-screen log of the detected host channels and every event, which helps when wiring up a new host. Opened directly in a browser, with no host channel, the page also shows a short note explaining how to embed it.

Message envelope

The page posts every event as a message with this envelope:

{ "source": "qrgen", "version": 1, "type": "scan", "barcodes": [] }
type Extra fields When
ready engine ("worker" or "main") The camera is open and scanning started.
scan barcodes: Barcode[] New codes passed the duplicate filter.
track tracked: TrackedBarcode[] Batch mode, after every decoded frame.
select barcode, selected, selection Batch results selection changed.
state state idle, starting, scanning, paused, stopped, error.
error code, message Starting failed. See error codes.

Always check source === "qrgen" before handling a message; other scripts in the page may post messages too. parseBridgeMessage() does this for you (see below).

Host channels

The page posts each message to every channel it finds, so the same page works in every host:

Host Channel in the page What the host receives
React Native (react-native-webview) window.ReactNativeWebView.postMessage(json) onMessage event, nativeEvent.data is the JSON string
Flutter (webview_flutter) window.QRGenBridge.postMessage(json) A JavaScriptChannel named QRGenBridge
iOS / macOS WKWebView window.webkit.messageHandlers.qrgen.postMessage(json) A WKScriptMessageHandler registered as qrgen
Windows WebView2, .NET MAUI on Windows window.chrome.webview.postMessage(json) WebMessageReceived, TryGetWebMessageAsString()
Android WebView window.QRGenAndroid.postMessage(json) A @JavascriptInterface object added as QRGenAndroid
<iframe> window.parent.postMessage(object, "*") A message event on the parent window; event.data is an object
Popup window.opener.postMessage(object, "*") A message event on the opener

Native channels receive the message as a JSON string; iframe and popup hosts receive an object.

Host-to-page commands

Control the scanner by calling window.qrgen.command(command) through your WebView’s JavaScript evaluation API (evaluateJavaScript, evaluateJavascript, ExecuteScriptAsync, injectJavaScript, runJavaScript), or by posting { source: "qrgen-host", ...command } into the page (iframes, React Native postMessage, Android postWebMessage).

Command Effect
{ "type": "start" } Start the camera (after autostart=0 or stop).
{ "type": "stop" } Stop and release the camera.
{ "type": "pause" } Stop decoding, keep the preview.
{ "type": "resume" } Continue, for example after a single mode scan.
{ "type": "torch" } Toggle the torch. With "value": true or false, set it.
{ "type": "switchCamera" } Switch between front and back cameras.
{ "type": "zoom", "value": 2 } Set the zoom factor.
{ "type": "options", "value": { "mode": "batch", "symbologies": "qr" } } Set element attributes (kebab-case names, string values) at runtime.
// From a WebView evaluation API:
window.qrgen.command({ type: "torch", value: true });

// From an iframe host:
iframe.contentWindow.postMessage({ source: "qrgen-host", type: "resume" }, "https://rockyljewell.github.io");

window.qrgen.channels lists the host channels the page detected, which helps when debugging.

Host examples

iframe

<iframe
  id="scanner"
  src="https://rockyljewell.github.io/QR-GEN/embed/?symbologies=qr,ean13&mode=single"
  allow="camera"
  style="width: 100%; height: 480px; border: 0"
></iframe>
<button id="again">Scan again</button>

<script>
  const ORIGIN = "https://rockyljewell.github.io";
  const frame = document.getElementById("scanner");

  window.addEventListener("message", (event) => {
    if (event.origin !== ORIGIN || event.data?.source !== "qrgen") return;
    if (event.data.type === "scan") console.log(event.data.barcodes[0].data);
    if (event.data.type === "error") console.warn(event.data.code, event.data.message);
  });

  document.getElementById("again").addEventListener("click", () => {
    frame.contentWindow.postMessage({ source: "qrgen-host", type: "resume" }, ORIGIN);
  });
</script>

allow="camera" is required for a cross-origin iframe to use the camera, and the parent page must itself be a secure context. On your own site, the web components are simpler than an iframe; the embed page is useful when you can’t add scripts to the host page.

iOS (WKWebView)

Add NSCameraUsageDescription to Info.plist, then:

import UIKit
import WebKit

final class ScannerViewController: UIViewController, WKScriptMessageHandler, WKUIDelegate {
    private var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        let config = WKWebViewConfiguration()
        config.allowsInlineMediaPlayback = true
        config.mediaTypesRequiringUserActionForPlayback = []
        config.userContentController.add(self, name: "qrgen")

        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        webView.uiDelegate = self
        view.addSubview(webView)
        webView.load(URLRequest(url: URL(string: "https://rockyljewell.github.io/QR-GEN/embed/?symbologies=qr,ean13&mode=single")!))
    }

    func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let json = message.body as? String,
              let object = try? JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any],
              object["source"] as? String == "qrgen" else { return }
        if object["type"] as? String == "scan",
           let barcode = (object["barcodes"] as? [[String: Any]])?.first,
           let data = barcode["data"] as? String {
            print("Scanned", data)
        }
    }

    // iOS 15+: grant the page's camera request for this origin without WebKit's extra prompt.
    @available(iOS 15.0, *)
    func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        decisionHandler(origin.host == "rockyljewell.github.io" ? .grant : .prompt)
    }

    func send(_ commandJSON: String) {
        webView.evaluateJavaScript("window.qrgen && window.qrgen.command(\(commandJSON))")
    }
}

WKUserContentController retains its handlers; remove the qrgen handler when you dismiss the scanner. For a native scanner without a WebView, see the iOS SDK.

Android (WebView)

Declare <uses-permission android:name="android.permission.CAMERA" /> in the manifest, then:

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import org.json.JSONObject

class ScannerActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private val url = "https://rockyljewell.github.io/QR-GEN/embed/?symbologies=qr,ean13&mode=single"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        webView = WebView(this)
        setContentView(webView)

        webView.settings.javaScriptEnabled = true
        webView.settings.mediaPlaybackRequiresUserGesture = false
        webView.addJavascriptInterface(Bridge(), "QRGenAndroid")
        webView.webChromeClient = object : WebChromeClient() {
            override fun onPermissionRequest(request: PermissionRequest) {
                val wantsCamera = request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
                if (wantsCamera && hasCameraPermission()) request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
                else request.deny()
            }
        }

        if (hasCameraPermission()) webView.loadUrl(url)
        else requestPermissions(arrayOf(Manifest.permission.CAMERA), 1)
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        webView.loadUrl(url) // the page shows camera-permission-denied if the user refused
    }

    private fun hasCameraPermission() =
        ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED

    fun command(json: String) = webView.evaluateJavascript("window.qrgen && window.qrgen.command($json)", null)

    inner class Bridge {
        @JavascriptInterface
        fun postMessage(json: String) {
            val message = JSONObject(json)
            if (message.optString("source") != "qrgen") return
            if (message.optString("type") == "scan") {
                val data = message.getJSONArray("barcodes").getJSONObject(0).getString("data")
                runOnUiThread { Toast.makeText(this@ScannerActivity, data, Toast.LENGTH_SHORT).show() }
            }
        }
    }
}

@JavascriptInterface methods run on a background thread; switch to the main thread before touching views. For a native scanner, see the Android SDK.

Windows (WebView2, WinUI and WPF)

using System.Text.Json;
using Microsoft.Web.WebView2.Core;

await webView.EnsureCoreWebView2Async();

webView.CoreWebView2.PermissionRequested += (_, e) =>
{
    if (e.PermissionKind == CoreWebView2PermissionKind.Camera && new Uri(e.Uri).Host == "rockyljewell.github.io")
        e.State = CoreWebView2PermissionState.Allow;
};

webView.CoreWebView2.WebMessageReceived += (_, e) =>
{
    using var doc = JsonDocument.Parse(e.TryGetWebMessageAsString());
    var root = doc.RootElement;
    if (root.GetProperty("source").GetString() != "qrgen") return;
    if (root.GetProperty("type").GetString() == "scan")
        Console.WriteLine(root.GetProperty("barcodes")[0].GetProperty("data").GetString());
};

webView.CoreWebView2.Navigate("https://rockyljewell.github.io/QR-GEN/embed/?symbologies=qr,ean13&mode=single");

// Commands
await webView.CoreWebView2.ExecuteScriptAsync("window.qrgen && window.qrgen.command({ type: 'pause' })");

Send commands with ExecuteScriptAsync, or post them with PostWebMessageAsJson("{\"source\":\"qrgen-host\",\"type\":\"pause\"}"). The bridge listens on window.chrome.webview too. For .NET MAUI on every platform, see the .NET SDK.

React Native and Flutter

The React Native package includes QRGenWebScanner, which wraps the embed page in react-native-webview, and the Flutter package covers webview_flutter. Their guides show the setup, including camera permissions.

Self-host the embed page

Host your own copy to pin the SDK version, work offline inside an app bundle served over HTTPS, change the look, or avoid a third-party origin. The page is a <qrgen-scanner> element configured from the query string and connected to the bridge:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
    <title>Scanner</title>
    <style>
      html, body { margin: 0; height: 100%; background: #000; overflow: hidden; }
      qrgen-scanner { position: fixed; inset: 0; height: 100%; }
    </style>
  </head>
  <body>
    <script type="module">
      import { attributesFromQuery, configure, connectBridge } from "./qrgen.js"; // dist/browser/qrgen.js

      configure({ wasmBaseUrl: "./wasm/" }); // zxing_reader.wasm next to the page

      const scanner = document.createElement("qrgen-scanner");
      for (const [name, value] of Object.entries(attributesFromQuery(location.search))) {
        scanner.setAttribute(name, value);
      }
      connectBridge(scanner); // forward events to the host, accept commands
      document.body.append(scanner);
    </script>
  </body>
</html>

Copy node_modules/qrgen-sdk/dist/browser/qrgen.js next to the page and the wasm files into wasm/. Serve it over HTTPS (or from https://localhost in app shells like Capacitor). Call connectBridge() before adding the element to the page so no early event is missed.

With a bundler, import from the npm entry points instead:

import "qrgen-sdk/elements";
import { attributesFromQuery, connectBridge } from "qrgen-sdk/bridge";

qrgen-sdk/bridge

Export Signature Description
connectBridge (element, window?) => () => void Forwards the element’s ready, scan, track, select, statechange and error events to every host channel, defines window.qrgen.command() and listens for { source: "qrgen-host" } messages on window and document. Returns a function that disconnects everything.
parseBridgeMessage (data: unknown) => BridgeMessage | null Parses a message from the page (a JSON string or an object). Returns null unless it is a QRGen message. Use it in hosts.
postToHost (message: BridgeMessage, window?) => void Sends a message to every host channel in the window.
detectChannels (window?) => string[] Which host channels exist: react-native, flutter, wkwebview, webview2, android, iframe, opener.
attributesFromQuery (search: string) => Record<string, string> Maps an embed query string to <qrgen-scanner> attributes (camelCase and kebab-case names, 1/0 to true/false). Unknown parameters are ignored.

BridgeMessage and BridgeCommand are exported as TypeScript types. A typed host handler for an iframe or a JavaScript-based WebView host:

import { parseBridgeMessage, type BridgeMessage } from "qrgen-sdk/bridge";

function onHostMessage(raw: unknown) {
  const message: BridgeMessage | null = parseBridgeMessage(raw);
  if (!message) return;
  switch (message.type) {
    case "scan":
      console.log(message.barcodes.map((b) => b.data));
      break;
    case "error":
      console.warn(message.code, message.message);
      break;
  }
}

window.addEventListener("message", (event) => onHostMessage(event.data));
Esc

Share this page

Embed bridge · Docs | QRGen

Star QRGen on GitHub Add a "Powered by QRGen" badge