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:
Overview of Building a Cordova QR Code Scanner
- Create a new Cordova project.
- Show camera preview and capture video frames using the
getUserMedia
API. - Use Dynamsoft Barcode Reader (DBR) to read barcodes from video frames. A plugin is created in order to use existing native libraries of DBR.
- Modify DBR’s runtime settings for scanning QR codes.
- 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
-
Create a project named Scanner.
cordova create scanner com.example.scanner Scanner
-
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
-
Create a plugin project with plugman:
plugman create --name DBR --plugin_id cordova-plugin-dynamsoft-barcode-reader --plugin_version 0.0.1
-
Add iOS platform:
plugman platform add --platform_name ios
- Configure
podspec
inplugin.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>
-
Integrate the plugin with the scanner project so that we can debug it with Xcode:
cordova plugin add <path to the plugin>
-
Modify the
DBR.m
file to integrate DBR.-
Include the library.
#import <DynamsoftBarcodeReader/DynamsoftBarcodeSDK.h>
-
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
-
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."); } }
-
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; }
-
-
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
-
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; }
-
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