In previous articles, I introduced multiple ways to scan barcodes from an HTML-based web app: using JavaScript libraries, using third-party mobile apps and using bluetooth barcode scanners. Which possibility to choose, strongly depends on the specifics of your project.
In many cases, however, scanning a barcode is merely supposed to save the user from typing a long number into an input field. If that's what you are looking for, here is a pure JavaScript example using Bootstrap 3 and the awsome barcode reader library QuaggaJS.
Note, that in my example the live stream viewfinder currently only works on Firefox, because Chrome and Opera only offer their getUserMedia API to SSL-enabled websites, which mine is not (yet). But you can still use the code in your SSL-enabled project. To try out the camera on Chrome and Opera, please refer to the official demos for now. They have much more complicated (and flexible) code, but work just the same in the end.
In general, the trouble with camera access through JavaScript is, that the results strongly depend on the browser, the operating system and even the device hardware. In this example I use the getUserMedia API for the live stream, which is still very poorly supported. So I've also added an extra button for a fallback to the better supported File API, which allows the user to call the native camera app on mobile devices or the regular browser upload on desktops. Try it out on different devices and browsers to see what happens. For more details on differences between the APIs refer to this post.
Device compatibility
The whole idea of scanning barcodes with a built-in camera, of course, makes most sense on mobile devices. However, the quality of the camera stream and many important features like autofocus or brightness adjustment seem very device-dependent. I've been able to achieve good results with fairly recent top-level smartphones like Samsung Galaxy 5+, LG G3+, etc. But cheaper devices often have problems focusing when using the live stream. At the same time it's no problem when taking the same picture via native camera app.
Browser compatibility
The getUserMedia API works very differently even on modern browsers. For example, the autofocus of the camera only on Chrome and Opera. On the other hand both of them restrict the use of this API from to the secure HTTPS-protocol. Firefox is less picky, but, for some reason, it provieds a very dark and blurred picture, so the recognition only works from time to time. I guess, Chrome is the best choince for now, but it still depends on the device too.
The Code
Now, let's see how this works. First of all, you need to include QuaggaJS on your page: just follow the official installation instructions. Now simply copy-paste the code below to get it working. Read the comments in the demo code for more information.
<div class="row"> <div class="col-lg-6"> <div class="input-group"> <input id="scanner_input" class="form-control" placeholder="Click the button to scan an EAN..." type="text" /> <span class="input-group-btn"> <button class="btn btn-default" type="button" data-toggle="modal" data-target="#livestream_scanner"> <i class="fa fa-barcode"></i> </button> </span> </div><!-- /input-group --> </div><!-- /.col-lg-6 --> </div><!-- /.row --> <div class="modal" id="livestream_scanner"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> <h4 class="modal-title">Barcode Scanner</h4> </div> <div class="modal-body" style="position: static"> <div id="interactive" class="viewport"></div> <div class="error"></div> </div> <div class="modal-footer"> <label class="btn btn-default pull-left"> <i class="fa fa-camera"></i> Use camera app <input type="file" accept="image/*;capture=camera" capture="camera" class="hidden" /> </label> <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button> </div> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> </div><!-- /.modal -->
<script type="text/javascript" src="assets/js/libs/quagga.min.js"></script> <style> #interactive.viewport {position: relative; width: 100%; height: auto; overflow: hidden; text-align: center;} #interactive.viewport > canvas, #interactive.viewport > video {max-width: 100%;width: 100%;} canvas.drawing, canvas.drawingBuffer {position: absolute; left: 0; top: 0;} </style> <script type="text/javascript"> $(function() { // Create the QuaggaJS config object for the live stream var liveStreamConfig = { inputStream: { type : "LiveStream", constraints: { width: {min: 640}, height: {min: 480}, aspectRatio: {min: 1, max: 100}, facingMode: "environment" // or "user" for the front camera } }, locator: { patchSize: "medium", halfSample: true }, numOfWorkers: (navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 4), decoder: { "readers":[ {"format":"ean_reader","config":{}} ] }, locate: true }; // The fallback to the file API requires a different inputStream option. // The rest is the same var fileConfig = $.extend( {}, liveStreamConfig, { inputStream: { size: 800 } } ); // Start the live stream scanner when the modal opens $('#livestream_scanner').on('shown.bs.modal', function (e) { Quagga.init( liveStreamConfig, function(err) { if (err) { $('#livestream_scanner .modal-body .error').html('<div class="alert alert-danger"><strong><i class="fa fa-exclamation-triangle"></i> '+err.name+'</strong>: '+err.message+'</div>'); Quagga.stop(); return; } Quagga.start(); } ); }); // Make sure, QuaggaJS draws frames an lines around possible // barcodes on the live stream Quagga.onProcessed(function(result) { var drawingCtx = Quagga.canvas.ctx.overlay, drawingCanvas = Quagga.canvas.dom.overlay; if (result) { if (result.boxes) { drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height"))); result.boxes.filter(function (box) { return box !== result.box; }).forEach(function (box) { Quagga.ImageDebug.drawPath(box, {x: 0, y: 1}, drawingCtx, {color: "green", lineWidth: 2}); }); } if (result.box) { Quagga.ImageDebug.drawPath(result.box, {x: 0, y: 1}, drawingCtx, {color: "#00F", lineWidth: 2}); } if (result.codeResult && result.codeResult.code) { Quagga.ImageDebug.drawPath(result.line, {x: 'x', y: 'y'}, drawingCtx, {color: 'red', lineWidth: 3}); } } }); // Once a barcode had been read successfully, stop quagga and // close the modal after a second to let the user notice where // the barcode had actually been found. Quagga.onDetected(function(result) { if (result.codeResult.code){ $('#scanner_input').val(result.codeResult.code); Quagga.stop(); setTimeout(function(){ $('#livestream_scanner').modal('hide'); }, 1000); } }); // Stop quagga in any case, when the modal is closed $('#livestream_scanner').on('hide.bs.modal', function(){ if (Quagga){ Quagga.stop(); } }); // Call Quagga.decodeSingle() for every file selected in the // file input $("#livestream_scanner input:file").on("change", function(e) { if (e.target.files && e.target.files.length) { Quagga.decodeSingle($.extend({}, fileConfig, {src: URL.createObjectURL(e.target.files[0])}), function(result) {alert(result.codeResult.code);}); } }); }); </script>