ใช้ Web แสกน QR Code (URL) กัน

แทนที่จะต้องเปิด Camera App เพื่อแสกน QR Code (URL) แล้วก็โดดไปที่ Web อีกที งั้นเราก็เปิด Web แล้ว แสกน QR Code (URL) ไปเลยดีกว่า

Siwawes Wongcharoen
3 min readJan 9, 2021

สืบเนื่องมาจากว่า ไม่ค่อยจะชอบ Flow การใช้งานของ ไทยชนะ ซักเท่าไร เนื่องจาก เราต้องเริ่มจาก เปิดแอพกล้อง -> Scan QR -> เปิดเว็บจาก URL ที่ Scan ได้ -> กดปุ่ม Checkin บนเว็บ

นอกจากนี้ในขั้นตอน Checkout ก็ต้องวนทำในลักษณะเดิม ไม่สามารถเปิดหน้าผลการ Checkin ค้างไว้แล้วค่อยกด Checkout ได้ หรือแม้แต่การทำ Favorite สถานที่ที่เราไปบ่อย ๆ ก็ไม่สามารถทำได้

ถึงแม้ว่าความ Flow ไม่ดีดังกล่าว หลายอย่าง มันจะดีขึ้น ถ้าโหลดแอพมาใช้ แต่เราไม่ได้อยากโหลดมาไง

สรุปที่ไม่ชอบจากไทยชนะ คือ

  1. ไม่ชอบการใช้งานอะไรที่ต้องโดดไป ๆ มา ๆ ระหว่างแอพ
  2. ไม่ได้อยากโหลดแอพเพิ่ม
  3. ฟีเจอร์ และ ​​Flow ที่เราอยากให้มันเป็น สามารถทำได้ในระดับเว็บ

จาก 3 ข้อหลักดังกล่าว จึงเป็นที่มาของ Mini Project ที่อยากจะทำให้ได้ ดังนี้

  1. เปิดแค่เว็บ แล้วจบ ไม่ต้องโดดไปแอพอื่น
  2. มีอะไรที่ช่วยให้เราทำ Checkout ได้ง่ายขึ้น
  3. ทำ Favorite ได้ แบบ ไม่ต้องใช้ Server เก็บข้อมูล

Blog นี้ทำอะไรบ้าง

จาก 3 ข้อที่อยากจะทำให้ได้ ใน Blog นี้จะอธิบายแค่ ข้อ 1. เพียงข้อเดียว เพราะข้อ 2. และ 3. สามารถใช้ Web Local Storage ทำได้แน่ ๆ

เปิดแค่เว็บ แล้วจบ ไม่ต้องโดดไปแอพอื่น

เริ่มจากขยายความตรงนี้ก่อน จากเดิม

เปิดแอพกล้อง -> Scan QR -> เปิดเว็บจาก URL ที่ Scan ได้ -> กดปุ่ม Checkin บนเว็บ

จะเปลี่ยนเป็น

เปิดเว็บที่เปิดกล้องพร้อม Scan QR ได้ -> Scan QR -> [Save URL to Local Storage] -> Redirect ไปที่ URL ที่ Scan ได้ -> กดปุ่ม Checkin บนเว็บ

ฉะนั้นเป้าหมาย หลักของ Blog นี้จะเป็นการ ทำให้ Web สามารถ Scan QR ได้ สิ่งที่ต้องการคือ ใช้งาน MediaDevices คู่กับ Barcode Detection API

MediaDevices

เป็น Feature สำคัญที่เอาไว้ เปิดกล้อง แล้ว Stream Video ผ่าน HTML Video Element

<video id="camera" width="100" height="100" autoplay></video><script>
async function openDefaultCamera () {
const constraints = (window.constraints = {
audio: false,
video: true
});

try {
const stream = await navigator.mediaDevices.getUserMedia(constraints)
const cameraElement = document.getElementById("camera")
cameraElement.srcObject = stream
} catch (error) {
throw error
}
}
</script>

วิธีที่ง่ายที่สุด คือ สั่งเปิดกล้อง ผ่าน mediaDevices.getUserMedia โดยใช้ navigator.mediaDevices.getUserMedia(constraints) โดยใส่ Constraints ที่คิดว่าจะต้องใช้ และตัวอุปกรณ์รองรับ ในกรณีที่เราสั่งเป็น video: true ในมือถือน่าจะได้กล้องหน้าเป็นส่วนใหญ่

แต่ถ้าจะเอากล้องหลัง ? ก็เปลี่ยน Constraints เป็น

const constraints = (window.constraints = {
audio: false,
video: { facingMode: { exact: "environment" } }
});

ในสภาพการใช้งานจริง เรารู้ว่า ตัวอุปกรณ์ที่เปิดเว็บนี้รองรับอะไรบ้าง จาก MediaDevices.getSupportedConstraints() ด้วยคำสั่งประมาณนี้

const supportedConstraints = navigator.mediaDevices.getSupportedConstraints()for (let const supportedConstraint of supportedConstraints) {
// TODO: describe supportedConstraint
}

Barcode Detection API

เว็บมี Barcode Detection API มาให้ใช้ ภายใต้ข้อกำจัดเล็ก ๆ น้อย ๆ เช่น ไม่ได้ Support ในทุก Browser …

และนอกจากนี้ ยังมีจุดที่น่าปวดหัวอยู่อีก เพราะตาม Doc เขียนไว้ว่า

Detection is achieved through the detect() method, which takes an image object; either an element, a Blob, ImageData or a CanvasImageSource. Optional parameters can be passed to the BarcodeDetector constructor to provide hints on which barcode formats to detect.

แต่พอทำจริง โยน Blob เข้าไปแล้ว Error … ช่างมัน ใช้ Canvas แทนก็ได้

Concept ตรงนี้คือ ต้องหาทาง Capture image ออกมาจาก Video ที่ Steam มาได้ แล้วเอา image ที่ได้ ไปแปะใน Canvas Element ก่อน

<video id="camera" width="100" height="100" autoplay></video>
<canvas id="canvas"></canvas>
<script>
function capture () {
const canvas = document.getElementById('canvas')
const video = document.getElementById('camera')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
canvas.getContext('2d').drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
}
</script>

ตรงนี้ถ้าอยากให้ทำ Auto Capture ก็ใช้ sleep() ช่วยได้

จากนั้น ก็โยน Canvas Element เข้า BarcodeDetector

<canvas id="canvas"></canvas><script>
async function detectQR () {
try {
const barcodeDetector = new BarcodeDetector({formats: ['qr_code']})
const barcodes = await barcodeDetector.detect(canvas)
for (const barcode of barcodes) {
// TODO: describe barcode -> barcode.rawValue
}
} catch (error) {
throw error
}
}
</script>

ตัว BarcodeDetector สามารถอ่าน Barcode ต่าง ๆ ได้ดังนี้ ส่วน Browser ตัวไหน อ่านอะไรได้บ้าง แนะนำให้ใช้ BarcodeDetector.getSupportedFormats() ตรวจก่อน จะดีกว่า

สรุป

เพียงเท่านี้ เราก็สามารถ อ่านจากค่าจาก QR Code ผ่าน Web ได้แล้ว สามารถทดลองเล่นได้ที่ https://puuga.github.io/web-qr/ และสามารถศึกษาตัว Source code ที่พอจะทำงานได้ ได้ที่ https://github.com/puuga/web-qr/blob/master/index.html

ปัญหาที่พบ

  • บน Safari iOS ตัว Video เหมือนละ Steam ไม่ได้ แต่ดันสามารถ Capture image ออกมาได้
  • ตัว barcodeDetector.detect ไม่ค่อยเคลียร์ว่ารับอะไรได้บ้างกันแน่
  • ตัว barcodeDetector ใช้บน Safari iOS ไม่ได้ WTF

--

--