I built a barcode scanner for Oracle APEX without installing a single plugin
No native extensions, no paid plugin, just the browser's camera and a bit of JavaScript. Here's the whole thing, step by step.
I built this for a check-in system where scanning tickets manually was too slow. No plugin, no paid SDK just the browser's camera, a bit of JavaScript, and PL/SQL on the APEX side.
Set up the page and the camera window
Blank APEX page (mine is page 17). Add a static content region with a video tag that's where the camera feed shows up.
<div id="viewfinder-container" style="text-align: center; max-width: 500px; margin: 0 auto;">
<video id="barcode-video" autoplay playsinline style="width: 100%; border-radius: 8px; border: 2px solid #0076df; background: #000;"></video>
<div id="scanner-controls" style="margin-top: 12px;">
<button id="btn-toggle-camera" type="button" class="t-Button t-Button--hot t-Button--icon t-Button--iconLeft">
<span class="t-Icon fa fa-camera" aria-hidden="true"></span>Start Scanner
</button>
</div>
</div>
Add three display-only items below it: P17_SCANNED_CODE, P17_ATTENDEE_NAME, P17_STATUS.
Add a fallback for iPhones
Chrome on Android reads barcodes natively, but iPhone Safari doesn't. ZXing.js fills that gap it only kicks in when native support is missing.
Page Properties → JavaScript → File URLs:
https://unpkg.com/@zxing/library@latest/umd/index.min.js
Write the PL/SQL that checks people in
The scanned code gets sent here through AJAX. It looks up the ticket, checks if the person's already inside, and either checks them in or warns you.
Processing → AJAX Callbacks → new process named PROCESS_SCAN:
declare
l_code varchar2(255) := apex_application.g_x01; -- Code passed from JS
l_attendee_name varchar2(150);
l_already_in varchar2(1);
begin
-- Look up attendee or inventory item
select full_name, checked_in_flag
into l_attendee_name, l_already_in
from event_attendees
where ticket_qr_code = l_code;
if l_already_in = 'Y' then
apex_json.open_object;
apex_json.write('success', false);
apex_json.write('message', 'Warning: ' || l_attendee_name || ' is already checked in!');
apex_json.close_object;
else
-- Update database record
update event_attendees
set checked_in_flag = 'Y',
check_in_time = systimestamp
where ticket_qr_code = l_code;
-- Return JSON success response
apex_json.open_object;
apex_json.write('success', true);
apex_json.write('message', 'Checked in: ' || l_attendee_name);
apex_json.write('attendee_name', l_attendee_name);
apex_json.write('status', 'CONFIRMED');
apex_json.close_object;
end if;
exception
when no_data_found then
apex_json.open_object;
apex_json.write('success', false);
apex_json.write('message', 'Invalid Code: Ticket not found.');
apex_json.close_object;
when others then
apex_json.open_object;
apex_json.write('success', false);
apex_json.write('message', 'Error: ' || sqlerrm);
apex_json.close_object;
end;
The table behind it, just five columns:
create table event_attendees (
attendee_id number generated by default as identity primary key,
full_name varchar2(150) not null,
ticket_qr_code varchar2(255) unique not null,
checked_in_flag varchar2(1) default 'N',
check_in_time timestamp
);
Build the scanner engine in JavaScript
Turns the camera on, watches for a barcode, and the moment it finds one, pauses the camera and calls PROCESS_SCAN. A cooldown stops the same code firing twice.
Page Properties → JavaScript → Function and Global Variable Declaration:
var ApexScanner = {
videoElement: null,
stream: null,
isScanning: false,
cooldown: false,
lastScannedCode: null,
detector: null,
start: async function() {
this.videoElement = document.getElementById('barcode-video');
try {
this.stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
this.videoElement.srcObject = this.stream;
this.isScanning = true;
this.cooldown = false;
if ('BarcodeDetector' in window) {
this.detector = new BarcodeDetector({
formats: ['qr_code', 'code_128', 'ean_13', 'upc_a']
});
this.scanNative();
} else {
this.scanZXing();
}
} catch (err) {
apex.message.showErrors([{
type: 'error',
location: 'page',
message: 'Unable to access camera: ' + err.message,
unsafe: false
}]);
}
},
stop: function() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
}
this.isScanning = false;
// Reset button UI
const btn = document.getElementById('btn-toggle-camera');
if (btn) {
btn.innerHTML = '<span class="t-Icon fa fa-camera"></span> Scan Next Code';
}
},
scanNative: async function() {
if (!this.isScanning) return;
try {
const barcodes = await this.detector.detect(this.videoElement);
if (barcodes.length > 0 && !this.cooldown) {
const code = barcodes[0].rawValue;
// Don't re-trigger for the exact same code
if (code !== this.lastScannedCode) {
this.handleCodeFound(code);
}
}
} catch (e) {}
if (this.isScanning) {
requestAnimationFrame(() => this.scanNative());
}
},
scanZXing: function() {
const codeReader = new ZXing.BrowserMultiFormatReader();
codeReader.decodeFromVideoDevice(null, 'barcode-video', (result, err) => {
if (result && !this.cooldown && this.isScanning) {
if (result.text !== this.lastScannedCode) {
this.handleCodeFound(result.text);
}
}
});
},
handleCodeFound: function(codeValue) {
this.cooldown = true;
this.lastScannedCode = codeValue;
// 1. AUTO-PAUSE CAMERA IMMEDIATELY ON DETECT
this.stop();
// 2. Set item value
apex.item('P17_SCANNED_CODE').setValue(codeValue);
// 3. Fire AJAX Lookup
apex.server.process(
'PROCESS_SCAN',
{ x01: codeValue },
{
dataType: 'json',
success: (pData) => {
if (pData.success) {
apex.message.showPageSuccess(pData.message);
apex.item('P17_ATTENDEE_NAME').setValue(pData.attendee_name);
apex.item('P17_STATUS').setValue(pData.status);
} else {
apex.message.showErrors([{
type: 'error',
location: 'page',
message: pData.message,
unsafe: false
}]);
}
}
}
);
}
};
P17_... / P17_... to your actual page number, or the values won't show up.Hook up the button
Listens for the button click and toggles the camera on/off, swapping the button text to match.
Page Properties → JavaScript → Execute when Page Loads:
// Toggle Scanner Button Listener
document.getElementById('btn-toggle-camera').addEventListener('click', function() {
if (ApexScanner.isScanning) {
ApexScanner.stop();
this.innerHTML = '<span class="t-Icon fa fa-camera"></span> Start Scanner';
} else {
ApexScanner.start();
this.innerHTML = '<span class="t-Icon fa fa-stop"></span> Stop Scanner';
}
});
| Column | What it stores |
|---|---|
| attendee_id | Auto-generated unique ID for each attendee |
| full_name | The attendee's name |
| ticket_qr_code | The unique code printed on their ticket |
| checked_in_flag | Y or N, whether they've already entered |
| check_in_time | Timestamp of when they checked in |
Comments
Post a Comment