Newer
Older
import {css, html} from 'lit-element';
import DBPLitElement from 'dbp-common/dbp-lit-element';
import * as commonStyles from 'dbp-common/styles';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import {Icon, MiniSpinner} from 'dbp-common';
import {classMap} from 'lit-html/directives/class-map.js';
/**
* Returns the ID for the most important device
*
* @param {Map} devices
*/
function getPrimaryDevice(devices) {
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
if (devices.has('environment'))
return 'environment';
}
if (devices.size) {
return Array.from(devices)[0][0];
}
return null;
}
/**
* Returns a map of device IDs and translated names.
*
* Moreimportant devices first.
*
* @returns {Map<string,string>} the map of devices
*/
async function getVideoDevices() {
let devices_map = new Map();
if (navigator.mediaDevices
&& navigator.mediaDevices.enumerateDevices
&& navigator.mediaDevices.getUserMedia) {
let devices;
try {
devices = await navigator.mediaDevices.enumerateDevices();
} catch (err) {
console.log(err.name + ": " + err.message);
return devices_map;
}
for (let device of devices) {
if (device.kind === 'videoinput') {
let id = device.deviceId;
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
devices_map.set('environment', i18n.t('back-camera'));
devices_map.set('user', i18n.t('front-camera'));
} else {
devices_map.set(id ? id : true, device.label || i18n.t('camera') + (devices_map.size + 1));
}
}
}
return devices_map;
} else {
return devices_map;
}
}
/**
* Notification web component
*/
export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
constructor() {
super();
this.lang = 'de';
this.scanIsOk = false;
this.showOutput = false;
this.activeCamera = '';
this.sourceChanged = false;
this._devices = new Map();
this._requestID = null;
this._loadingMessage = '';
}
static get scopedElements() {
return {
'dbp-icon': Icon,
'dbp-mini-spinner': MiniSpinner,
};
}
/**
* See: https://lit-element.polymer-project.org/guide/properties#initialize
*/
static get properties() {
return {
lang: { type: String },
askPermission: { type: Boolean, attribute: false },
videoRunning: { type: Boolean, attribute: false },
front: { type: Boolean, attribute: false },
loading: { type: Boolean, attribute: false },
scanIsOk: { type: Boolean, attribute: 'scan-is-ok' },
showOutput: { type: Boolean, attribute: 'show-output' },
stopScan: { type: Boolean, attribute: 'stop-scan' },
activeCamera: { type: String, attribute: false },
sourceChanged: { type: Boolean, attribute: false },
clipMask: { type: Boolean, attribute: 'clip-mask' },
_devices: { type: Map, attribute: false},
_loadingMessage: { type: String, attribute: false },
};
}
connectedCallback() {
super.connectedCallback();
i18n.changeLanguage(this.lang);
this.updateComplete.then(async ()=>{
this._loadingMessage = i18n.t('no-camera-access');
let devices = await getVideoDevices();
this.activeCamera = getPrimaryDevice(devices) || '';
if (!this.stopScan) {
disconnectedCallback() {
this.stopScanning();
super.disconnectedCallback();
}
updated(changedProperties) {
if (changedProperties.get('stopScan') && !this.stopScan) {
}
/**
* Init and start the video and QR code scan
*
*/
this.stopScan = false;
let video = document.createElement("video");
let canvasElement = this._("#canvas");
let canvas = canvasElement.getContext("2d");
let loadingMessage = this._("#loadingMessage");
let outputContainer = this._("#output");
let outputMessage = this._("#outputMessage");
let outputData = this._("#outputData");
let qrContainer = this._("#qr");
Steinwender, Tamara
committed
let color = this.scanIsOk ? getComputedStyle(this)
.getPropertyValue('--dbp-success-bg-color') : getComputedStyle(this)
.getPropertyValue('--dbp-danger-bg-color');
function drawLine(begin, end, color) {
canvas.beginPath();
canvas.moveTo(begin.x, begin.y);
canvas.lineTo(end.x, end.y);
canvas.lineWidth = 4;
canvas.strokeStyle = color;
canvas.stroke();
}
let videoId = this.activeCamera;
let constraint = { video: { deviceId: videoId } };
if ( (videoId === 'environment' || videoId === '') ) {
console.log("vid:", videoId);
constraint = { video: { facingMode: "environment" } };
} else if ( videoId === 'user' ) {
console.log("vid2:", videoId);
constraint = { video: { facingMode: "user" } };
let stream = null;
try {
stream = await navigator.mediaDevices.getUserMedia(constraint);
} catch(e) {
console.log(e);
this.askPermission = true;
}
if (stream !== null) {
video.srcObject = stream;
video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
video.play();
this.videoRunning = true;
qrContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (this._requestID !== null) {
cancelAnimationFrame(this._requestID);
this._requestID = null;
console.assert(this._requestID === null);
this._requestID = requestAnimationFrame(tick);
}
let lastVideoScanTime = -1;
let lastCode = null;
that._requestID = null;
if (that.sourceChanged) {
video.srcObject.getTracks().forEach(function(track) {
track.stop();
console.log("Changed Media");
});
that.sourceChanged = false;
if (that.videoRunning === false) {
video.srcObject.getTracks().forEach(function(track) {
track.stop();
loadingMessage.hidden = false;
canvasElement.hidden = true;
outputContainer.hidden = true;
});
that._loadingMessage = i18n.t('no-camera-access');
if (that.stopScan) {
video.srcObject.getTracks().forEach(function(track) {
track.stop();
console.log("stop early");
loadingMessage.hidden = false;
canvasElement.hidden = true;
outputContainer.hidden = true;
});
that._loadingMessage = i18n.t('finished-scan');
return;
}
that.loading = true;
that._loadingMessage = i18n.t('loading-video');
if (video.readyState === video.HAVE_ENOUGH_DATA) {
loadingMessage.hidden = true;
canvasElement.hidden = false;
outputContainer.hidden = false;
canvasElement.height = video.videoHeight;
canvasElement.width = video.videoWidth;
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
let maskWidth = 0;
let maskHeight = 0;
let maskStartX = canvasElement.width;
let maskStartY = canvasElement.height;
let imageData;
if (that.clipMask) {
//draw mask
let clip = canvasElement.width > canvasElement.height ? canvasElement.height/4 * 3 : canvasElement.width/4 * 3;
maskWidth = clip < 250 ? 250 : clip;
maskHeight = clip < 250 ? 250 : clip;
clip = clip < 250 ? 250 : clip;
maskStartX = canvasElement.width/2 - maskWidth/2;
maskStartY = canvasElement.height/2 - maskHeight/2;
canvas.beginPath();
canvas.fillStyle = "#0000006e";
canvas.moveTo(0,0);
canvas.lineTo( canvasElement.width, canvasElement.height);
canvas.rect(maskStartX, maskStartY, maskWidth, maskHeight);
canvas.fill();
canvas.beginPath();
canvas.fillStyle = "white";
canvas.moveTo(maskStartX,maskStartY);
canvas.rect(maskStartX, maskStartY, clip/3, 10);
canvas.rect(maskStartX, maskStartY, 10, clip/3);
canvas.rect(maskStartX + clip/3*2, maskStartY, clip/3, 10);
canvas.rect(maskStartX + clip - 10, maskStartY, 10, clip/3);
canvas.rect(maskStartX, maskStartY + clip -10, clip/3, 10);
canvas.rect(maskStartX, maskStartY + clip/3*2, 10, clip/3);
canvas.rect(maskStartX + clip/3*2, maskStartY + clip -10, clip/3, 10);
canvas.rect(maskStartX + clip - 10, maskStartY + clip/3*2, 10, clip/3);
canvas.fill();
imageData = canvas.getImageData(maskStartX , maskStartY, maskWidth, maskHeight);
} else {
imageData = canvas.getImageData(0 , 0, canvasElement.width, canvasElement.height);
let code = null;
// We only check for QR codes 5 times a second to improve performance
let shouldAnalyze = Math.abs(lastVideoScanTime - video.currentTime) >= 1/3;
if (shouldAnalyze) {
lastVideoScanTime = video.currentTime;
code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
lastCode = code;
} else {
code = lastCode;
}
let topLeftCorner = {x: 0, y: 0};
let topRightCorner = {x: 0, y: 0};
let bottomRightCorner = {x: 0, y: 0};
let bottomLeftCorner = {x: 0, y: 0};
topLeftCorner.x = code.location.topLeftCorner.x + maskStartX;
topLeftCorner.y = code.location.topLeftCorner.y + maskStartY;
topRightCorner.x = code.location.topRightCorner.x + maskStartX;
topRightCorner.y = code.location.topRightCorner.y + maskStartY;
bottomRightCorner.x = code.location.bottomRightCorner.x + maskStartX;
bottomRightCorner.y = code.location.bottomRightCorner.y + maskStartY;
bottomLeftCorner.x = code.location.bottomLeftCorner.x + maskStartX;
bottomLeftCorner.y = code.location.bottomLeftCorner.y + maskStartY;
}
else {
topLeftCorner = code.location.topLeftCorner;
topRightCorner = code.location.topRightCorner;
bottomRightCorner = code.location.bottomRightCorner;
bottomLeftCorner = code.location.bottomLeftCorner;
}
drawLine(topLeftCorner, topRightCorner, color);
drawLine(topRightCorner, bottomRightCorner, color);
drawLine(bottomRightCorner, bottomLeftCorner, color);
drawLine(bottomLeftCorner, topLeftCorner, color);
outputMessage.hidden = true;
outputData.parentElement.hidden = false;
outputData.innerText = code.data;
if (lastSentData !== code.data)
that.sendUrl(code.data);
lastSentData = code.data;
} else {
outputMessage.hidden = false;
outputData.parentElement.hidden = true;
}
}
if (video.readyState === video.HAVE_ENOUGH_DATA && !scroll) {
qrContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
scroll = true;
}
console.assert(that._requestID === null);
that._requestID = requestAnimationFrame(tick);
/**
* Update if video source is changed
*
* @param e
*/
updateSource(e) {
this.activeCamera = e.srcElement.value;
this.sourceChanged = true;
}
/**
* Stops the active video and scan process
*
*/
/**
* Sends an event with the data which is detected from QR code reader
*
* @param data
*/
Steinwender, Tamara
committed
sendUrl(data) {
const event = new CustomEvent("dbp-qr-code-scanner-data",
{ bubbles: true, composed: true , detail: data});
}
static get styles() {
// language=css
return css`
${commonStyles.getThemeCSS()}
${commonStyles.getGeneralCSS()}
${commonStyles.getButtonCSS()}
text-align: center;
padding: 40px;
}
.wrapper-msg {
width: 100%;
display: flex;
justify-content: center;
align-items: baseline;
margin-top: 2rem;
margin-top: 20px;
background: #eee;
padding: 10px;
padding-bottom: 0;
padding-bottom: 10px;
word-wrap: break-word;
text-align: center;
}
.spinner{
margin-right: 10px;
font-size: 0.7em;
}
#videoSource{
padding-bottom: calc(0.375em - 2px);
padding-left: 0.75em;
padding-right: 1.75em;
padding-top: calc(0.375em - 2px);
background-position-x: calc(100% - 0.4rem);
font-size: inherit;
}
#videoSource:hover {
background: calc(100% - 0.2rem) center no-repeat url("https://mw-frontend-dev.tugraz.at/apps/checkin/local/dbp-common/icons/chevron-down.svg");
color: black;
background-position-x: calc(100% - 0.4rem);
background-size: auto 45%;
select:not(.select)#videoSource{
background-size: auto 45%;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid black;
@media only screen
and (orientation: portrait)
and (max-device-width: 765px) {
.button-wrapper{
display: flex;
justify-content: space-between;
}
}
let hasDevices = this._devices.size > 0;
return html`
<div class="columns">
<div class="column" id="qr">
<div class="${classMap({hidden: !hasDevices})}">
<button class="start button is-primary ${classMap({hidden: this.videoRunning})}" @click="${() => this.startScanning()}" title="${i18n.t('start-scan')}">${i18n.t('start-scan')}</button>
<button class="stop button is-primary ${classMap({hidden: !this.videoRunning})}" @click="${() => this.stopScanning()}" title="${i18n.t('stop-scan')}">${i18n.t('stop-scan')}</button>
<select id="videoSource" class="button" @change=${this.updateSource}>
${Array.from(this._devices).map(item => html`<option value="${item[0]}">${item[1]}</option>`)}
</select>
<div id="loadingMessage" class=" ${classMap({hidden: !this.askPermission})}">
<div class="wrapper-msg">
<dbp-mini-spinner class="spinner ${classMap({hidden: !this.loading})}"></dbp-mini-spinner>
<div class="loadingMsg">${this._loadingMessage}</div>
<canvas id="canvas" hidden class=""></canvas>
<div id="output" hidden class=" ${classMap({hidden: !this.showOutput})}">
<div id="outputMessage">${i18n.t('no-qr-detected')}</div>
<div hidden><b>${i18n.t('data')}:</b> <span id="outputData"></span></div>
<div class="${classMap({hidden: hasDevices})}">
</div>
</div>
`;
}
}