Last active
October 28, 2022 20:41
-
-
Save bobvanderlinden/a58f39e44dba53e17cd2e2ec4774af08 to your computer and use it in GitHub Desktop.
Simple QR-Code scanner based on jsqrcode
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script src="https://webqr.com/llqrcode.js"></script> | |
<script src="jsqrcode-camera.js"></script> | |
</head> | |
<body> | |
<div id="qrcodescanner"></div> | |
<div id="message"></div> | |
</body> | |
<script> | |
window.onload = function() { | |
QRCodeScanner({ | |
element: document.getElementById('qrcodescanner'), | |
width: 400, | |
height: 300, | |
onScanSuccess: function(result) { | |
console.log('Scan Success', result); | |
document.getElementById('message').textContent = result; | |
}, | |
onScanError: function(error) { | |
console.log('Scan Error', error); | |
document.getElementById('message').textContent = 'No QR Code found'; | |
} | |
}); | |
} | |
</script> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function QRCodeScanner(options) { | |
const rootElement = options.element; | |
const width = options.width; | |
const height = options.height; | |
const onScanSuccess = options.onScanSuccess; | |
const onScanError = options.onScanError; | |
const scanInterval = options.scanInterval || 100; | |
let stream = undefined; | |
let deviceId = undefined; | |
let availableVideoInputs = undefined; | |
function c(tagName, attributes, children) { | |
const element = document.createElement(tagName); | |
Object.assign(element, attributes); | |
replaceChildren(element, children); | |
return element; | |
} | |
function replaceChildren(parent, children) { | |
parent.innerHTML = ''; | |
appendChildren(parent, children); | |
} | |
function appendChildren(parent, children) { | |
(children || []).forEach(function(child) { | |
if (typeof child === 'string') { | |
child = document.createTextNode(child); | |
} | |
parent.appendChild(child); | |
}); | |
} | |
function activatePanel(panel) { | |
panel.parentNode.childNodes.forEach(function(node) { | |
node.style.display = panel === node ? 'block' : 'none'; | |
}); | |
} | |
const loadingPanel = c('div', { | |
className: 'loading panel' | |
}, []); | |
Object.assign(loadingPanel.style, { | |
position: 'absolute', | |
left: 0, | |
right: 0, | |
top: 0, | |
bottom: 0 | |
}); | |
const errorPanel = c('div', { | |
className: 'error panel' | |
}, []); | |
Object.assign(errorPanel.style, { | |
position: 'absolute', | |
left: 0, | |
right: 0, | |
top: 0, | |
bottom: 0 | |
}); | |
const switchVideoInputButton = c('button', { | |
className: 'switch button', | |
onclick: handleSwitchVideoInputButtonClick | |
}); | |
const controlsOverlayElement = c('div', { | |
className: 'controls' | |
}, [ | |
switchVideoInputButton | |
]); | |
Object.assign(controlsOverlayElement.style, { | |
position: 'absolute', | |
left: 0, | |
right: 0, | |
top: 0, | |
bottom: 0 | |
}); | |
const videoElement = c('video', { | |
autoplay: true | |
}); | |
Object.assign(videoElement.style, { | |
width: width + 'px', | |
height: height + 'px' | |
}); | |
const videoPanel = c('div', { | |
className: 'video panel' | |
}, [ | |
videoElement, | |
controlsOverlayElement | |
]); | |
Object.assign(videoPanel.style, { | |
position: 'relative', | |
left: 0, | |
right: 0, | |
top: 0, | |
bottom: 0 | |
}); | |
const URL = window.URL || window.webkitURL || window.mozURL || window.msURL; | |
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; | |
function initializeUserMedia() { | |
if (!navigator.getUserMedia) { | |
errorPanel.textContent = 'Camera not supported'; | |
activatePanel(errorPanel); | |
} | |
navigator.getUserMedia({ | |
video: { | |
facingMode: 'environment', | |
deviceId: deviceId | |
? { exact: deviceId } | |
: undefined | |
} | |
}, handleUserMediaSuccess, handleUserMediaError); | |
} | |
function handleUserMediaSuccess(stream) { | |
activatePanel(videoPanel); | |
if (availableVideoInputs === undefined) { | |
retrieveVideoInputs(); | |
} | |
videoElement.srcObject = stream; | |
scheduleScan(); | |
} | |
function retrieveVideoInputs() { | |
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { | |
switchVideoInputButton.style.display = 'none'; | |
} | |
navigator.mediaDevices.enumerateDevices() | |
.then(function(devices) { | |
availableVideoInputs = devices | |
.filter(function(device) { | |
return device.kind === 'videoinput'; | |
}); | |
switchVideoInputButton.style.display = availableVideoInputs.length < 2 | |
? 'none' | |
: 'block'; | |
}); | |
} | |
function handleUserMediaError(error) { | |
errorPanel.textContent = 'Error activating camera'; | |
activatePanel(errorPanel); | |
} | |
function handleSwitchVideoInputButtonClick() { | |
let currentAvailableVideoInputIndex = -1; | |
availableVideoInputs.forEach(function(videoinput, index) { | |
if (videoinput.deviceId === deviceId) { | |
currentAvailableVideoInputIndex = index; | |
} | |
}); | |
const newAvailableVideoInputIndex = (currentAvailableVideoInputIndex + 1) % availableVideoInputs.length; | |
deviceId = availableVideoInputs[newAvailableVideoInputIndex].deviceId; | |
initializeUserMedia(); | |
} | |
// Scanning QR code from camera at set interval. | |
// We reuse a premade canvas to copy the camera image to | |
// and convert the canvas to an ObjectUrl to pass it to jsqrcode. | |
var scanTimeout; | |
const canvas = c('canvas', { | |
width: width, | |
height: height | |
}); | |
const context = canvas.getContext('2d'); | |
function scheduleScan() { | |
clearTimeout(scanTimeout); | |
scanTimeout = setTimeout(scan, scanInterval); | |
} | |
function stopScan() { | |
clearTimeout(scanTimeout); | |
scanTimeout = undefined; | |
} | |
qrcode.callback = handleScanResult; | |
function handleScanResult(result) { | |
onScanSuccess(result); | |
} | |
function scan() { | |
if (!videoElement.parentNode) { | |
// Stop scanning when videoElement was detached. | |
return; | |
} | |
context.drawImage(videoElement, 0, 0, width, height); | |
// We set these two, so that jsqrcode won't | |
// look for an qrcode element using getElementById. | |
qrcode.canvas_qr2 = canvas; | |
qrcode.qrcontext2 = context; | |
try { | |
qrcode.decode(); | |
} catch(e) { | |
// jsqrcode throws an error each time an qrcode was not found. | |
onScanError(e); | |
} | |
scheduleScan(); | |
} | |
// Initialization of QRCodeScanner | |
Object.assign(rootElement.style, { | |
position: 'relative', | |
width: width + 'px', | |
height: height + 'px', | |
backgroundColor: 'black' | |
}); | |
appendChildren(rootElement, [ | |
errorPanel, | |
loadingPanel, | |
videoPanel | |
]); | |
activatePanel(loadingPanel); | |
initializeUserMedia(); | |
return { | |
stop: stopScan | |
} | |
} |
Hi. I think there is an issue with iOS devices, isn't it? I can't solve it. In my case on iOS Devices the scanner doesn't work. I makes a black picture by opening the scanner. On other devices it works very well. Maybe you can solve it?
I unfortunately do not have an iOS device to test this. Is it a Safari problem in general that also exists on OSX?
the problem exists in safari or chrome as well.
It should be fixed now. URL.createObjectURL
was deprecated for usage of MediaStreams. Now uses video.srcObject = stream;
.
Now it directly makes a picture. I can't position the camera over a qr-code.
Hi guys ! The issue with iOS always exists. Do you have a solution ?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi, how do you get this code to recognise it has a qr code and then process it to give you the data for instance in a text box...?
Thanks, Jerry