Build a Cordova QR Code Scanner

Apache Cordova is a tool to create cross-platform apps from standard web technologies (HTML, CSS, and JavaScript). Its primary purpose is to provide a bridge for native device API access and to bundle for distribution.1 In this article, we are going to use Cordova to build a QR code scanner that runs on browsers, Android devices and iOS devices.

A screenshot of the final QR code scanner demo:

screenshot

Overview of Building a Cordova QR Code Scanner

  1. Create a new Cordova project.
  2. Show camera preview and capture video frames using the getUserMedia API.
  3. Use Dynamsoft Barcode Reader (DBR) to read barcodes from video frames. A plugin is created in order to use existing native libraries of DBR.
  4. Modify DBR’s runtime settings for scanning QR codes.
  5. Add flashlight control.

Getting started with Dynamsoft Barcode Reader

Environment

  • NPM
  • Cordova
  • Gradle and Android Studio for Android development
  • CocoaPods, Xcode for iOS development

Create a new project

  1. Create a project named Scanner.

     cordova create scanner com.example.scanner Scanner
    
  2. Add platforms and run to have a test:

     cordova platform add browser
     cordova run browser
    

Show camera preview using getUserMedia

The getUserMedia API allows access to media devices like camera and microphone. We can use it to show camera preview and capture video frames.

Code to show camera preview:

navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
    var camera = document.getElementsByClassName("camera")[0]; //A video element
    // Attach the stream to the video element
    camera.srcObject = stream;
}).catch(function(err) {
    console.error('getUserMediaError', err, err.stack);
});

Constraints are required as a parameter for getUserMedia. We can specify which camera and which resolution to use. Here we use the back camera using facingMode if running on a mobile device and set the default resolution as 1280x720.

var constraints;
var platform = window.device.platform;
if (platform == "browser"){
    constraints = {
        video: true,
        audio: false
    }
}else{
    constraints = {
        video: {facingMode: { exact: "environment" }},
        audio: false
    }
}
constraints.video.width = {exact: 1280};
constraints.video.height = {exact: 720};

The cordova-plugin-device plugin is used to detect which platform the program is running on.

It is installed via the following command:

cordova plugins add cordova-plugin-device

The entire HTML file:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="Content-Security-Policy" content="default-src * blob:; script-src * 'unsafe-eval' 'unsafe-inline' blob:; style-src * 'unsafe-inline'; media-src *; img-src 'self' data:">
        <meta name="format-detection" content="telephone=no">
        <meta name="msapplication-tap-highlight" content="no">
        <meta name="viewport" content="initial-scale=1, width=device-width, viewport-fit=cover">
        <meta name="color-scheme" content="light dark">
        <link rel="stylesheet" href="css/index.css">
        <title>Hello World</title>
    </head>
    <body>
         <video class="camera" muted autoplay="autoplay" playsinline="playsinline" webkit-playsinline style="width:100%;height:100%;position:absolute;left:0;top:0;object-fit:fill;"></video>
        <script src="cordova.js"></script>
        <script>
        document.addEventListener('deviceready', onDeviceReady, false);
        function onDeviceReady() {
            // Cordova is now initialized. Have fun!
            //alert("device ready.");
            play();
        }
        
        function play(deviceId, HDUnsupported) {
            var constraints;
            var platform = window.device.platform;
            if (platform == "browser"){
                constraints = {
                    video: true,
                    audio: false
                }
            }else{
                constraints = {
                    video: {facingMode: { exact: "environment" }},
                    audio: false
                }
            }

            constraints.video.width = {exact: 1280};
            constraints.video.height = {exact: 720};
            
            navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
                var camera = document.getElementsByClassName("camera")[0];
                // Attach local stream to video element
                camera.srcObject = stream;

            }).catch(function(err) {
                console.error('getUserMediaError', err, err.stack);
            });
        }

        </script>
    </body>
</html>

Platform Quicks

Extra steps are needed to run the above page on Android and iOS devices.

Ask for Camera Permission for Android

Here, we use cordova-plugin-android-permissions to ask for camera permissions.

Install the plugin:

cordova plugin add cordova-custom-config

Request permission:

function askForAndroidPermissionsAndPlay(){
    var permissions = cordova.plugins.permissions;
    permissions.hasPermission(permissions.CAMERA, function (status) {
        if (status.hasPermission) {
            //CAMERA permission already granted
            console.log("Permission granted!");
            play();
        }
        else {
            // need to request camera permission
            permissions.requestPermission(permissions.CAMERA, success, error);

            function error() {
                // camera permission not turned on
                alert('Please accept the Android permissions.');
            }

            function success(status) {
                if (status.hasPermission) {
                    // user accepted
                    alert("Permission granted!");
                    play();
                }
            }
        }
    });
}

We also need to add the uses-permission to the AndroidManifest.xml file. We can do this by adding the following to the config.xml:

<platform name="android">
    <allow-intent href="market:*" />
    <config-file parent="/*" target="AndroidManifest.xml">
        <uses-permission android:name="android.permission.CAMERA" />
    </config-file>
</platform>

Remember to add xmlns:android="http://schemas.android.com/apk/res/android" to the root widget or you may encounter unbound prefix" in android config.xml error.

Enable the getUserMedia API for iOS

The WKWebView used in iOS does not support WebRTC until iOS 14.3 and does not allow using it in origins other than HTTPS until iOS 14.52.

For compatibility concerns, we can install the iosrtc plugin to enable the getUserMedia API on most iOS devices.

Install the plugin:

cordova plugin add cordova-plugin-iosrtc

Then, expose the API in the global namespace like regular browsers:

if (window.device.platform === 'iOS') {
    cordova.plugins.iosrtc.registerGlobals();
}

In addition, we need to add the following to config.xml to request permissions:

<platform name="ios">
    <config-file parent="NSCameraUsageDescription" target="*-Info.plist">
        <string>For barcode scanning</string>
    </config-file>
    <config-file parent="NSMicrophoneUsageDescription" target="*-Info.plist">
        <string>For barcode scanning</string>
    </config-file>
<platform/>

Please note that the camera view is a UIView on top of the webview. We can use z-index and set the webview’s color to transparent to layout HTML elements above the camera view. You can learn more about it here: video css.

Read barcodes from video frames

Dynamsoft Barcode Reader has JavaScript, iOS and Android libraries. So it is possible to decode barcode images on these platforms.

Decode in browsers

Since the JS version of DBR uses WebAssembly, which requires HTTP, it is not possible to access it like local files, we need to use it by including the following tag.

<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.0.0/dist/dbr.js"></script>
<!-- apply for a key here: https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform
<script>
Dynamsoft.DBR.BarcodeReader.license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==";
</script>

Then, we can create a reader instance and read barcodes from video frames.

var interval;
var initialized=false;
var decoding=false;
var platform;
var reader;
function onDeviceReady() {
    initDBR();
}
async function initDBR(){
    reader = await Dynamsoft.DBR.BarcodeReader.createInstance();
    initialized = true;
    startDecoding();
}

function startDecoding(){
    clearInterval(interval)
    interval = setInterval(captureAndDecode, 1000);
}

function captureAndDecode(){
    var video = document.getElementsByClassName("camera")[0];
    if (initialized==true) {
        if (decoding==true){
            //Still decoding. Skip.
            return;
        }
        if (platform == "iOS"){
            video.render.save(function (data) {
                decode(data); //network error
            });
        }else{
            decode(video);
        }
        
    }
}

async function decode(data){
    decoding=true;
    var results = [];
    results = await reader.decode(data); //supports dataurl, base64, arraybuffer, video element, img element, etc
    decoding=false;
    alert("Found "+results.length+" barcode(s)");
}

DBR JS also works on Android. However, it cannot work on iOS as the limited network ability of iOS’s webview causes a network error. (PS: this has been fixed for iOS 15)

We can create a Cordova plugin to use the native iOS framework of DBR to solve this and also, the native framework has a better performance.

Decode on iOS

We need to create a Cordova plugin to use the iOS framework of DBR.

Write a plugin

  1. Create a plugin project with plugman:

     plugman create --name DBR --plugin_id cordova-plugin-dynamsoft-barcode-reader --plugin_version 0.0.1
    
  2. Add iOS platform:

     plugman platform add --platform_name ios
    
  3. Configure podspec in plugin.xml to use the cocoapods of DBR according to the doc.
     <podspec>
         <pods use-frameworks="true">
             <pod name="DynamsoftBarcodeReader" spec="~> 9.0.0" />
         </pods>
     </podspec>
    
  4. Integrate the plugin with the scanner project so that we can debug it with Xcode:

     cordova plugin add <path to the plugin>
    
  5. Modify the DBR.m file to integrate DBR.

    1. Include the library.

       #import <DynamsoftBarcodeReader/DynamsoftBarcodeSDK.h>
      
    2. Define properties and methods.

       @interface DBR: CDVPlugin<DBRLicenseVerificationListener>
         // Member variables go here.
       @property (nonatomic, retain) DynamsoftBarcodeReader* barcodeReader;
       @property Boolean decoding;
       - (void)init:(CDVInvokedUrlCommand*)command;
       - (void)decode:(CDVInvokedUrlCommand*)command;
       @end
      
    3. Add init method which initializes DBR with a license. You can apply for a trial license here.

       - (void)init:(CDVInvokedUrlCommand*)command
       {
           [self.commandDelegate runInBackground:^{
               NSString* license = [command.arguments objectAtIndex:0];
               [self initDBR: license];
               CDVPluginResult* result = [CDVPluginResult
                                              resultWithStatus: CDVCommandStatus_OK
                                          messageAsString: self->_barcodeReader.getVersion
                                              ];
                          
               [[self commandDelegate] sendPluginResult:result callbackId:command.callbackId];
           }];
                  
       }
              
       - (void)initDBR: (NSString*) license{
           if (_barcodeReader == nil){
               NSLog(@"%s", "Initializing");
               _barcodeReader = [[DynamsoftBarcodeReader alloc] initWithLicense:license];
           }else{
               NSLog(@"%s", "Already initialized.");
           }
       }
      
    4. Add a decode method which decodes base64-encoded images and returns the barcode text, barcode format and localization info.

       - (void)decode:(CDVInvokedUrlCommand*)command
       {
           [self.commandDelegate runInBackground:^{
               NSString* base64 = [command.arguments objectAtIndex:0];
               NSArray<NSDictionary*> *array = [self decodeBase64: base64];
               CDVPluginResult* result = [CDVPluginResult
                                              resultWithStatus: CDVCommandStatus_OK
                                              messageAsArray: array
                                              ];
                          
               [[self commandDelegate] sendPluginResult:result callbackId:command.callbackId];
           }];
                  
       }
      
       - (NSArray<NSDictionary*>*)decodeBase64: (NSString*) base64 {
           NSMutableArray<NSDictionary*> * resultsArray = [[ NSMutableArray alloc] init];
           if (_barcodeReader != nil && _decoding==false){
               @try {
                   _decoding=true;
                   NSError __autoreleasing * _Nullable error;
                   NSArray<iTextResult*>* results = [_barcodeReader decodeBase64:base64 error:&error];
                   _decoding=false;
                   for (iTextResult* result in results) {
                       CGPoint p1 = [result.localizationResult.resultPoints[0] CGPointValue];
                       CGPoint p2 = [result.localizationResult.resultPoints[1] CGPointValue];
                       CGPoint p3 = [result.localizationResult.resultPoints[2] CGPointValue];
                       CGPoint p4 = [result.localizationResult.resultPoints[3] CGPointValue];
                       NSDictionary *dictionary = @{
                              @"barcodeText" : result.barcodeText,
                              @"barcodeFormat" : result.barcodeFormatString,
                              @"x1" : @(p1.x),
                              @"y1" : @(p1.y),
                              @"x2" : @(p2.x),
                              @"y2" : @(p2.y),
                              @"x3" : @(p3.x),
                              @"y3" : @(p3.y),
                              @"x4" : @(p4.x),
                              @"y4" : @(p4.y)
                       };
                       [resultsArray addObject:(dictionary)];
                   }
               }
               @catch (NSException *exception) {
                   NSLog(@"Exception:%@",exception);
               }
               @finally{
                   NSLog(@"Skip");
               }
           }
           NSArray<NSDictionary *> *array = [resultsArray copy];
           return array;
       }
      
  6. Modify DBR.js to expose these methods:

     var exec = require('cordova/exec');
    
     exports.decode = function (arg0, success, error) {
         exec(success, error, 'DBR', 'decode', [arg0]);
     };
    
     exports.init = function (arg0, success, error) {
         exec(success, error, 'DBR', 'init', [arg0]);
     };
    

All right, the plugin is completed. We can use it in our projects. You can learn more about Cordova plugins by checking its docs. Creating an Android plugin is much the same.

Use the plugin

  1. Initialize DBR.

     async function initDBR(){
         if (platform == "iOS"){
             cordova.plugins.DBR.init("<license key>",onInit);
         }else{
             reader = await Dynamsoft.DBR.BarcodeReader.createInstance();
             onInit();
         }
     }
        
     function onInit(){
         initialized=true;
     }
    
  2. Decode base64.

     function captureAndDecode(){
         var video = document.getElementsByClassName("camera")[0];
         if (initialized==true) {
             if (decoding==true){
                 //Still decoding. Skip.
                 return;
             }
             if (platform == "iOS"){
                 video.render.save(function (data) {
                     decode(data);
                 });
             }else{
                 decode(video);
             }
         }
     }
        
     function onDecode(results){
         decoding=false;
         alert("Found "+results.length+" barcode(s)");
     }
        
     async function decode(data){
         decoding=true;
         if (platform=="iOS"){
             cordova.plugins.DBR.decode(data,onDecode);
         }else{
             var results = [];
             results = await reader.decode(data);
             onDecode(results);
         }
     }
    

Modify the settings to read QR codes

Dynamsoft Barcode Reader provides rich parameters that users can customize and optimize for different usage scenarios for the best scanning performance.

We can modify its parameters with a JSON template using its initRuntimeSettingsWithString method.

For example, we can modify the settings to scan QR code only and here is the JSON template:

{"ImageParameter":{"BarcodeFormatIds":["BF_QR_CODE"],"Description":"","Name":"Settings"},"Version":"3.0"}

Use the template:

var template = "{\"ImageParameter\":{\"BarcodeFormatIds\":[\"BF_QR_CODE\"],\"Description\":\"\",\"Name\":\"Settings\"},\"Version\":\"3.0\"}";
if (platform == "iOS"){
    cordova.plugins.DBR.initRuntimeSettingsWithString(template);
}else{
    reader.initRuntimeSettingsWithString(template);
}

We also need to wrap the initRuntimeSettingsWithString method in the iOS plugin.

In the DBR.m file, add the following code:

- (void)initRuntimeSettingsWithString:(CDVInvokedUrlCommand*)command
{

    NSString* template = [command.arguments objectAtIndex:0];
    NSError __autoreleasing * _Nullable error;
    [_barcodeReader initRuntimeSettingsWithString:template conflictMode:EnumConflictModeOverwrite error:&error];
    CDVPluginResult* result = [CDVPluginResult
                                   resultWithStatus: CDVCommandStatus_OK
                                   messageAsBool: true
                                   ];
        
    [[self commandDelegate] sendPluginResult:result callbackId:command.callbackId];
}

In the DBR.js file, add the following code:

exports.initRuntimeSettingsWithString = function (arg0, success, error) {
    exec(success, error, 'DBR', 'initRuntimeSettingsWithString', [arg0]);
};

Add flashlight control

Turning on the flashlight is useful for QR code scanning in a low-light environment.

We can enable it with the following code:

const track = stream.getVideoTracks()[0];
track.applyConstraints({
        advanced: [{torch: true}]
      });

This only works on Chrome 59+ on Android and Desktop3. So the limitation of using getUserMedia is that we cannot control the flashlight on iOS and early versions of Android. For such a case, we need to use native camera preview. The latest plugin has Dynamsoft Camera Enhancer included to do this. It will display the camera preview behind the webview so that we can still modify the interface. The native camera preview can also provide more camera controls, like zoom and focus.

Source Code

https://github.com/xulihang/cordova-barcode-scanner

References