A decoding library that only takes image buffers reads codes beautifully and shows nothing: no scan screen, no framing overlay, no result hand-off. If your Android app used to launch a scanner app by intent and the new library expects raw frames, the missing half is a camera preview loop you own. This guide supplies that loop with numbers: 640×480 analysis frames at 15-30 fps as the default, a pixels-per-module rule that sets working distance, the YUV hand-off into the decoder, an accept-once gate, and the release checks that catch the failures users find first. It is written for app developers and controls engineers adding a scan step to an Android helper device on the floor, and it sits with the rest of the code-reading guides. Where a dedicated reader beats code, the last section says so with parts.
Why doesn't a buffer-only barcode library come with a scanning screen?
Because the scan-intent route hands the whole job to a separate scanner app — one that owns the camera, draws the framing overlay and shows the result screen — while a buffer-only library only turns an image you hand it into text.
That split decides the architecture. The intent call is a finished product: one line starts a second app that requests the camera, draws the reticle, decodes, shows the result and returns the string. The library call is one step of that pipeline — pixels in, text out — deliberately blind to cameras so it can sit behind any preview stack. The other three jobs land on you: camera access with a preview the user can aim, a per-frame hand-off into the decoder, and a gate so one physical scan yields one result rather than thirty events a second. None is difficult; together it comes to roughly a hundred lines around the decode call, and the sections below size each job with numbers.
One boundary case: if your client runs in a browser instead of a native app, the trade-offs differ in kind, and mobile app versus browser barcode scanning maps where each stops working.
What replaces the scan intent in your own app?
Four pieces replace it: a camera preview configured for image analysis, a per-frame callback, a worker-thread decoder call, and an accept-once gate that stops the camera — the decode itself stays one library call per frame, and the rest is plumbing you can test step by step.
Wire the pieces in this order. Each step leaves something you can test before starting the next.
- Build the library in: compile the decoder core as a native Android library or adopt an existing wrapper, and confirm its decode call accepts a byte buffer, a width and a height, and returns text or nothing. That signature is the contract every later step feeds.
- Declare the camera permission and request it at runtime, handling a refusal before any preview work — most black-preview reports start here.
- Start a 640×480 preview through an image-analysis use case (CameraX ImageAnalysis is the common one), so frames reach your code and not only the screen.
- In the frame callback, take the luminance plane: at 640×480 it is 307,200 bytes, one per pixel — the grayscale image the decoder wants.
- Decode on a worker thread. While a decode runs, drop incoming frames and keep only the newest; the thread must never fall behind.
- Put the accept-once gate between decoder and UI before the first test, so a code held in view cannot flood the screen with results.
- On an accepted result, stop and release the camera for one-shot duty, or reset the gate and keep scanning for continuous duty.
- Through commissioning, log attempts, results and decode time per frame. Those three numbers point at distance, light or resolution without guesswork.
Which preview resolution and frame rate should you request?
Request 640×480 at 15-30 fps, because 30 fps gives a 33 ms budget per frame and a 640×480 grayscale decode typically needs only 5-20 ms of it, while a 1920×1080 preview carries 6.75× the pixels and can push one decode past a whole frame period.
Treat 5-20 ms as a planning range for a lean native core, then measure: decode ten frames on your device and use your number. The scaling is the reliable part, because decode cost grows about with pixel count. A 1280×960 preview holds 4× the pixels of 640×480 (1,228,800 / 307,200), and 1920×1080 holds 6.75× (2,073,600 / 307,200). So a 135 ms decode at 30 fps falls about four frames behind for every frame it reads — the preview stays smooth while results go stale. Step up from 640×480 only when the pixels-per-module check below fails at your working distance, and then one step, not to maximum.
How many pixels does a code need in the frame?
Give every module at least 2 pixels, 3-4 when you can: below 2 px per module the decoder samples between bars and starts missing narrow elements, and a 21×21 version-1 QR with its quiet zone spans 29 modules, so 58 px minimum and 87 px comfortable.
Worked example: a version-1 QR code is 21×21 modules; with the 4-module quiet zone each side it spans 29 modules. The 2 px floor sets a target of 29 × 2 = 58 px. Take a phone main camera at 65° horizontal field of view — typical for that camera — and a code at d = 200 mm. World width: W = 2 × 200 mm × tan 32.5° = 2 × 200 × 0.637 = 255 mm. The code must cover 58 of the frame's 640 px: 58 / 640 × 255 mm = 23 mm. At the comfortable 3 px per module the target is 29 × 3 = 87 px, and 87 / 640 × 255 mm = 35 mm. Decision: at 200 mm, place that QR at least 23 mm wide; 35 mm buys margin.
Field of view moves the number most. A wide camera at 90° sees W = 2 × 200 mm × tan 45° = 400 mm at the same distance, so the same 23 mm code spans 23 / 400 × 640 = 37 px — 37 / 29 = 1.3 px per module, under the floor. Halve the distance instead: at about 127 mm, W = 254 mm and the 58 px target again means 23 mm. Distance and field of view trade one for the other; the pixels-per-module check settles it.
How do you turn the preview frame into decoder input?
Android delivers the preview as YUV 4:2:0, and its first plane is already the 8-bit grayscale most decoding libraries want — 307,200 bytes at 640×480, one per pixel, no colour conversion — so the conversion step is a copy plus two traps: the row stride and per-device layout.
The stride is the row length the camera actually allocates, and it can exceed the frame width when the driver pads rows. Slice the buffer row by row, or pass stride alongside width if your wrapper takes one; a stride read as width shows up as diagonal shear, and codes stop decoding at a few pixels of difference. Devices also differ in which YUV arrangement the analysis callback holds, so read the layout from the frame rather than assuming a constant. The principle — feed the decoder pixel data, not bitmaps or compressed previews — is the same one behind fixing decoder input type errors and feeding pixel data instead of bitmaps on desktop. Prove your pipeline once: dump one buffer to a file, open it as raw grayscale, and confirm the code sits where you think it does.
How do you accept one result once and stop the loop?
Decide the mode first: one-shot scanning accepts the first decode, stops the camera and returns the text; continuous reading keeps the camera open and suppresses repeats by holding the last text and refiring only on a different symbol or after a 2 s gap.
Skip the gate and a code held in view returns a result every frame — thirty identical events per second at 30 fps — and any listener that appends or writes multiplies rows. The one-shot gate is three fields: last text, timestamp, in-flight flag. Continuous reading fires only when the text differs from the last accepted value, or the same value returns after the window has closed.
For unattended reading, add one rule: accept only when two consecutive frames decode to the same text. At 30 fps that costs one extra frame — 33 ms — and removes the single-frame misreads a lone decode can produce. What a lone decode is worth is its own subject in judging whether a decode is trustworthy, and the listener storm it prevents is the one scan event handling without recursive updates untangles. Before any accepted text moves downstream, give it the treatment in camera barcode decoding: from image to validated data: check format, length and payload before anything writes.
How do you keep frames square, upright and readable at a tilt?
Preview pixels must stay square and upright: squeeze a 4:3 buffer into a 16:9 view and every module narrows by about a third, and decode a rotated buffer and 1D rows stop being rows. Match the view's aspect ratio to the buffer and normalise rotation before the decode call.
The aspect-ratio arithmetic: a 640×480 buffer is 4:3 (1.33) and a 16:9 view is 1.78, so filling the view stretches one axis by 1.78 / 1.33 = 1.34, a 34% relative stretch that bar widths and quiet zones inherit. Letterbox instead: keep pixels square and centre the spare area. On orientation, the sensor's rows need not match the screen's, so the analysis buffer can arrive turned 90° or 180° from what the user sees; a 1D code sampled along the wrong axis returns nothing at all, which is why the correction belongs before the decode call, as in correcting camera frames before decoding. Tilt itself is forgiving — decoders read 1D codes at a fair skew — but keep the code in the middle third of the frame, where the lens is flattest, and keep its quiet zones in view.
When does the decode belong on a fixed industrial code reader?
When the decode moves from a hand to a production line, a fixed industrial code reader repeats the whole loop for you — lens, illumination, trigger and decoder in one housing, reporting text over Ethernet or a discrete output; handheld readers cover the walks-to-the-code case.
That is the right split while the scan step lives in an operator's hand: variable aim, variable light, human judgement. A production station inverts every one of those conditions — fixed distance, fixed angle, no hands — and fixed code readers are that inversion, productised. Where an operator walks to the code, handheld code readers supply the grip, trigger and aim assist, and nobody maintains the loop at all. Both families sit in the camera finder. Three line-side readers cover the common cases:
- Interface
- Fast Ethernet
- Resolution
- Up to 3 MP
- Interface
- Fast Ethernet
- Resolution
- 5 MP
- Interface
- GigE
- Resolution
- Up to 3 MP
The MV-ID3013PM-12M-WBN is the compact station pick; the LS-CRD-0028 adds throughput headroom with its 60 codes/s rating; the MV-ID813M-05S-WBN-SR brings its own white light for dark housings. For walk-to-the-code duty, the LS-HCR-0002 handheld pairs a 1.0 MP reader with a wireless Ethernet link.
- Resolution
- Up to 3 MP
- Shutter
- Global
- Interface
- Ethernet
- Connection
- Wireless
Whichever you choose, the remaining design question is the payload path from reader to controller, which barcode reader PLC communication plans end to end.
What should you test before the app ships?
Test the four things that break deployments: distance, motion, light and repetition. Each check below has a pass criterion you can write down before running it — for distance, that is the 23 mm code at 200 mm from the worked example.
- Distance sweep: read the smallest production code at 150 mm, 200 mm and 250 mm. Pass means the 2 px-per-module floor still holds at the distance you will actually run.
- Motion: scan while walking at a normal pace. If reads die, cap exposure at one frame period — 33 ms at 30 fps — and let gain rise instead of exposure.
- Light: dim to the darkest corner of the real site, enable the torch, and repeat the distance sweep with exposure still capped.
- Repetition: hold one code in view for five seconds — exactly one accepted result. Then ten different codes back to back — ten results.
- Worst-case symbols: print the code with the thinnest quiet zone and lowest error correction you will allow, and require the loop to read that one.
- Telemetry: attempts, results and decode time on every run; a healthy loop shows decode time inside the 33 ms budget and results within a handful of frames.
The loop is half the app. The operator-facing half — screens, states, what happens when the network drops — has its own shape, laid out in an Android helper app on the shop floor. When you would rather buy the read loop than write it, start at the system configurator, or send code size, symbology and working distance through the quote form and we will size the reader from those three numbers.
Questions
Should the decoder enable every symbology it supports?
No. Enable only the formats your codes actually use. Every enabled symbology adds work to each frame's decode, so a five-format sweep can push past the 33 ms budget at 30 fps where one format would not, and wrong-format hits become real data: a Code 39 read out of a partly visible Code 128 ends up in production records as a string nobody printed. Restricting the list is a start-up setting and removes both problems at once.
Why does the preview stay black on some devices?
Three usual causes, in order: the camera permission was never granted and the refusal went unhandled, another process holds the camera, or the requested analysis size is one the device does not support. Handle the permission result before binding the preview, and pick analysis sizes from the device's own supported list — 640×480 is close to universal, exotic sizes are not.
How long until the first read at 15 fps?
Each attempt costs 67 ms at 15 fps. With at least 2 px per module and steady light, a readable code typically decodes within three to six frames, so the first result lands in roughly 0.2-0.4 s. If it takes twice that, the cause is almost always pixels per module or flickering light — raise resolution or add light before touching the frame rate.
What should travel with the decoded text when you send it on?
At minimum: the text, the symbology, a timestamp and the device orientation, because downstream systems validate differently per code type. Run the validation before anything writes — check length, allowed character set and the check character the symbology provides — so a partial read dies inside the app instead of in production data.
Can you regression-test the decode loop without a device?
Yes. Dump twenty real frames to files once, on the worst device you own, and replay them through the same library in a desktop test build. A library update then proves itself against the exact buffers that behaved badly before, and the replay runs in milliseconds because it skips the camera entirely.



