A scanner looks like a small app. Open the camera, look at frames, decode. Two problems dominate the implementation and both are counter-intuitive the first time you meet them.

The scanner that freezes after a few seconds

The camera delivers frames to an analyser through a small pool of buffers. Your callback receives a frame, does something with it, and must release it so the buffer returns to the pool.

If you do not release it, the pool empties. There is no exception and no log line. Frames simply stop arriving, the preview keeps showing a live image because that is a separate stream, and the scanner is dead while looking entirely alive.

The number of frames you get before it stops is the pool size — a handful. So a scanner with a leaked frame works for a fraction of a second and then never decodes anything again, and the user's experience is "it scans sometimes, usually at the start."

Ours leaked on the error path. The happy path released the frame properly; a decode failure returned early. Since most frames in a normal scan do not contain a readable code, this meant the scanner died almost immediately in the field and worked in every test where a code was already in frame.

The fix is to release in a finally — every path, no exceptions — and the general lesson is that a resource with a small fixed pool and no error on exhaustion is one you want to release in exactly one place.

Every frame decodes, and that is the problem

The opposite failure, once frames flow correctly.

A camera produces frames continuously. Point it at a QR code and every frame for as long as you hold it there contains that code. A naive scanner fires a result on each one — thirty a second, all identical.

This is not a hypothetical annoyance. It means the result screen opens thirty times, or thirty rows land in the history, or on an app that auto-opens links, a browser gets thirty intents.

Debouncing on value alone is insufficient, because the correct behaviour differs by situation. Scanning the same parcel label twice in a row, deliberately, should produce two scans. Holding the camera on one code for two seconds should produce one.

The rule we landed on: a decoded value fires once, and the same value does not fire again while the code remains continuously in view. Losing the code — the camera moves away, the frame stops decoding for a moment — resets it. So holding steady gives you one result and deliberately re-scanning gives you another, which matches what people expect without them having to think about it.

Latency comes from resolution, and so does range

The other tuning decision is analysis resolution, and it is a genuine trade rather than a matter of picking the best value.

A higher resolution frame contains more detail, so a dense QR code, or one further away, or one printed small, is more likely to decode. It also takes longer to process, which means fewer decode attempts per second and a scanner that feels slower to respond on easy codes.

Most scans are easy: a large code, close, well lit, on a table or a poster. Optimising for those means a lower analysis resolution and a scanner that responds almost instantly.

We scan at a moderate resolution and increase it when a code appears to be present and is not decoding — when the frame has the structure of a code but the decode fails, that is the signal that more detail might help. Easy codes stay fast; difficult ones get a second chance without the user doing anything.

We also request continuous autofocus and use only the latest frame rather than queueing. A backlog is worse than useless in a scanner: by the time you decode a frame from half a second ago, the camera has moved.

Getting a difficult code to read

Some things help far more than others, and the ranking is not what people assume.

Angle beats distance. The most common failure is glare, not size. A code on a glossy menu, a laminated card, or a screen reflects the light source straight back and washes out part of the pattern. Tilting the phone a few degrees moves the reflection off the code. People instinctively move closer instead, which does not help and often makes the framing worse.

Hold still for a moment after framing. Autofocus needs a beat, and a moving frame is a blurred frame. A scanner that has not decoded within a second usually decodes immediately once the phone stops moving.

Fill about half the frame. Too far and the modules are smaller than the pixels resolving them. Too close and the camera cannot focus — most phone cameras have a minimum focus distance of several centimetres, and inside that everything is blurred. Closer is not always better and the failure at very close range looks identical to the failure at long range.

Keep the whole code in frame including its margin. QR codes are located by their corner patterns and the clear area around them. A code cropped at the edge, or one running right up against dark printing, is harder to find.

Flatten curved surfaces where you can. A code on a bottle or a curved package is geometrically distorted. Rotating the item so the code faces you directly works better than any camera setting.

Bad printing is sometimes just bad printing. A code printed too small, on a poor thermal printer, or reproduced from a photocopy may not be readable by anything. If several apps and several phones fail on the same code, the code is the problem.

After the scan

The result is where a scanner earns its keep, because the scan itself is rarely the task.

Check what the code actually says before acting on it. For a link, that means looking at the domain rather than at the text next to the code — a code can say anything, including that it points somewhere it does not. For a product code, that means confirming it matches what you were holding.

Keep the result when you will need it away from the original. A parcel label you will want during a return, a product code you want to compare in a shop, a link you cannot open right now. The alternative is walking back to the code, and the code is frequently on something you have already thrown away.

Clear what you do not need. Scan history is a record of where you have been and what you were looking at, and a code can carry a Wi-Fi password or an account credential. It is worth keeping short.