Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
CKeyAnalyzer.kt
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | package org.openckey.ckey.camerax |
| 2 | |
| 3 | import android.graphics.ImageFormat |
| 4 | import android.os.Handler |
| 5 | import android.os.Looper |
| 6 | import androidx.camera.core.ImageAnalysis |
| 7 | import androidx.camera.core.ImageProxy |
| 8 | import java.util.concurrent.Executor |
| 9 | import org.openckey.ckey.CKeyResult |
| 10 | import org.openckey.ckey.CKeyScanner |
| 11 | |
| 12 | /** |
| 13 | * A CameraX analyzer that decodes CKeys from the luma (Y) plane of each |
| 14 | * frame and drives a telemetry scan session (docs/adoption-and-telemetry.md): |
| 15 | * the session begins with the first frame (or [start]), every analyzed |
| 16 | * frame is counted with its processing time, the first decode succeeds it, |
| 17 | * and [stop] without a decode fails it. |
| 18 | * |
| 19 | * ``` |
| 20 | * val analyzer = CKeyAnalyzer { result -> showResult(result) } |
| 21 | * val analysis = ImageAnalysis.Builder() |
| 22 | * .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) |
| 23 | * .setResolutionSelector(...) // CKeys need ~6-11 camera px per symbol unit |
| 24 | * .build().apply { setAnalyzer(cameraExecutor, analyzer) } |
| 25 | * // when the camera is unbound / the screen goes away: |
| 26 | * analyzer.stop() |
| 27 | * ``` |
| 28 | * |
| 29 | * After a decode, frames are skipped until [scanAgain]. [onResult] runs on |
| 30 | * [callbackExecutor] (the main thread by default), never as an automatic |
| 31 | * action: show the payload and let the user choose (CKeyResult.action). |
| 32 | * Use the default YUV_420_888 output format. Call `CKey.install(context)` |
| 33 | * once (e.g. in Application.onCreate) so telemetry can also be flushed when |
| 34 | * the app goes to the background. |
| 35 | */ |
| 36 | class CKeyAnalyzer @JvmOverloads constructor( |
| 37 | private val callbackExecutor: Executor = MainThreadExecutor, |
| 38 | /** Minimum milliseconds between analyzed frames (50 = at most 20 fps, as the iOS reader). */ |
| 39 | private val minFrameIntervalMs: Long = 50, |
| 40 | private val onResult: (CKeyResult) -> Unit, |
| 41 | ) : ImageAnalysis.Analyzer { |
| 42 | |
| 43 | private val scanner = CKeyScanner() |
| 44 | private var lastFrameNs = 0L |
| 45 | |
| 46 | /** Begins a scan session (optional: the first frame begins one). */ |
| 47 | fun start() = scanner.start() |
| 48 | |
| 49 | /** Ends the scan session; call when the camera is unbound or the scanner UI goes away. */ |
| 50 | fun stop() = scanner.stop() |
| 51 | |
| 52 | /** Starts a new session after a decode. */ |
| 53 | fun scanAgain() = scanner.scanAgain() |
| 54 | |
| 55 | override fun analyze(image: ImageProxy) { |
| 56 | try { |
| 57 | if (scanner.hasResult) return |
| 58 | val now = System.nanoTime() |
| 59 | if (lastFrameNs != 0L && now - lastFrameNs < minFrameIntervalMs * 1_000_000L) return |
| 60 | lastFrameNs = now |
| 61 | if (image.format != ImageFormat.YUV_420_888 && image.format != ImageFormat.YUV_422_888 && |
| 62 | image.format != ImageFormat.YUV_444_888) return |
| 63 | val y = image.planes[0] |
| 64 | val result = scanner.processLuma(y.buffer, y.rowStride, y.pixelStride, image.width, image.height) |
| 65 | if (result != null) callbackExecutor.execute { onResult(result) } |
| 66 | } finally { |
| 67 | image.close() |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | private object MainThreadExecutor : Executor { |
| 72 | private val handler = Handler(Looper.getMainLooper()) |
| 73 | override fun execute(command: Runnable) { |
| 74 | handler.post(command) |
| 75 | } |
| 76 | } |
| 77 | } |