Record a Video using CameraX and Read Barcodes from Video Files
You can read barcodes and QR codes from video files on Android in two ways: decode frames while the video plays, or grab every frame with FFmpeg and decode it offline. This article builds a Kotlin app that records videos with CameraX and then reads barcodes from the recorded files with Dynamsoft Barcode Reader using both approaches.
Reading from video files matters when codes move past the camera (for example along a long shelf) or when you want to evaluate live-scanning performance: a video contains rich information, and a state-of-the-art phone can record 240 FPS footage at 1920x1080.
What you’ll build: an Android app with a CameraActivity that records video with the CameraX VideoCapture use case, and a VideoActivity that decodes the video in two modes: a live-emulation mode that snapshots the playing VideoView with PixelCopy, and a frame mode that extracts every frame with FFmpegFrameGrabber (JavaCV) and decodes each bitmap with CaptureVisionRouter (com.dynamsoft:barcodereaderbundle:11.6.2000).
Key Takeaways
- CameraX video recording needs the
VideoCaptureuse case built on aRecorderwith aQualitySelector; recording output goes to aMediaStoreOutputOptionsdestination. - Video decoding has two practical modes in the sample:
PixelCopysnapshots of the playingVideoView(emulates a live scan) andFFmpegFrameGrabberframe extraction (processes every frame, useful for benchmarks). - With the current SDK, decode each extracted bitmap through
cvr.capture(bitmap, EnumPresetTemplate.PT_READ_BARCODES); the decodedBarcodeResultItementries providetextandformatdirectly. - Restrict the barcode formats to what the test needs (EAN-13 and QR Code here) through the simplified settings of the preset template to avoid unnecessary decoding work.
Common Developer Questions
How do I record a video with CameraX?
Build a Recorder with a QualitySelector, create the VideoCapture use case with VideoCapture.withOutput(recorder), bind it to the lifecycle together with the Preview use case, and start recording with videoCapture.output.prepareRecording(...).start(...).
How do I read barcodes from a video file?
Two approaches are implemented in the sample. The first plays the video in a VideoView and copies the current frame with PixelCopy before decoding. The second uses FFmpegFrameGrabber from JavaCV to grab every frame, converts each frame to a rotated bitmap, and decodes it with cvr.capture(bitmap, EnumPresetTemplate.PT_READ_BARCODES).
Why restrict decoding to QR Code and EAN-13?
The sample compares Dynamsoft with ZXing, which is configured with POSSIBLE_FORMATS limited to QR Code and EAN-13. Limiting Dynamsoft to the same formats (EnumBarcodeFormat.BF_EAN_13 or EnumBarcodeFormat.BF_QR_CODE) keeps the comparison fair and speeds up decoding.
Which frame should I decode for a stable result?
In the live-emulation mode, decoding on a timer while the video plays gives the closest match to real live scanning. In the frame mode, decoding every frame and keeping the first successful result mirrors the benchmark statistics shown at the end of this article.
Prerequisites
- Android Studio with the Android SDK (API level 21 or higher).
- An Android device or emulator with a camera to record test videos.
- A Dynamsoft license key - the sample uses a time-limited trial key that requires a network connection on first use.
Get a 30-day free trial license
Record Video using CameraX
Let’s create a new Android project with Android Studio and add video recording function to it. The following parts take some code from Google’s CameraXVideo sample.
Add CameraX Dependencies
Open the project’s build.gradle, add the following to include CameraX:
// CameraX dependencies (first release for video is: "1.1.0-alpha10")
def camerax_version = "1.1.0-beta01"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
Request Permissions
-
Open
AndroidManifest.xml, add the following permissions:<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> -
In
MainActivity, request permissions.private var PERMISSIONS_REQUIRED = arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // add the storage access permission request for Android 9 and below. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { val permissionList = PERMISSIONS_REQUIRED.toMutableList() permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) PERMISSIONS_REQUIRED = permissionList.toTypedArray() } if (!hasPermissions(this)) { // Request camera-related permissions activityResultLauncher.launch(PERMISSIONS_REQUIRED) } } private fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } private val activityResultLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> // Handle Permission granted/rejected var permissionGranted = true permissions.entries.forEach { if (it.key in PERMISSIONS_REQUIRED && it.value == false) permissionGranted = false } if (!permissionGranted) { Toast.makeText(this, "Permission request denied", Toast.LENGTH_LONG).show() } }
Create a Fullscreen Activity for Video Recording
- Create a new empty activity named
CameraActivity -
Add CameraX’s preview control in the XML file:
<androidx.camera.view.PreviewView android:id="@+id/previewView" android:background="@color/purple_200" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="V,9:16"> </androidx.camera.view.PreviewView> -
Make the activity full screen:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) val decorView: View = window.decorView decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) }
A button in MainActivity is used to launch the CameraActivity.
Start Camera Preview
Let’s display the camera preview in CameraActivity first.
@SuppressLint("UnsafeOptInUsageError")
private suspend fun bindCaptureUsecase() {
val cameraProvider = ProcessCameraProvider.getInstance(this).await()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
var previewView = findViewById<PreviewView>(R.id.previewView)
previewView.updateLayoutParams<ConstraintLayout.LayoutParams> {
val orientation = baseContext.resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
dimensionRatio = "V,9:16"
}else{
dimensionRatio = "H,16:9"
}
}
val previewBuilder = Preview.Builder()
previewBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9)
val preview = previewBuilder.build().apply {
setSurfaceProvider(previewView.surfaceProvider)
}
try {
cameraProvider.unbindAll()
var camera = cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview
)
} catch (exc: Exception) {
exc.printStackTrace()
Toast.makeText(context,exc.localizedMessage,Toast.LENGTH_LONG).show()
goBack()
}
}
The PreviewView’s layout has to be adjusted according to the orientation.
Record Video with the VideoCapture Use Case
Let’s define a VideoCapture use case to add recording function.
-
Create a
Recorderand specifies the output video quality.var quality = Quality.HD val qualitySelector = QualitySelector.from(quality) var recorderBuilder = Recorder.Builder() recorderBuilder.setQualitySelector(qualitySelector) val recorder = recorderBuilder.build() -
Create a
VideoCaptureuse case using theRecorder.videoCapture = VideoCapture.withOutput(recorder) -
Add the
VideoCaptureuse case:var camera = cameraProvider.bindToLifecycle( this, cameraSelector, + videoCapture, preview ) -
Start recording and save the output as a file:
@SuppressLint("MissingPermission") private fun startRecording() { // create MediaStoreOutputOptions for our recorder: resulting our recording! val name = "CameraX-recording-" + SimpleDateFormat(FILENAMEFORMAT, Locale.US) .format(System.currentTimeMillis()) + ".mp4" val contentValues = ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, name) } val mediaStoreOutput = MediaStoreOutputOptions.Builder( this.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) .setContentValues(contentValues) .build() // configure Recorder and Start recording to the mediaStoreOutput. currentRecording = videoCapture.output .prepareRecording(this, mediaStoreOutput) .apply { if (audioEnabled) withAudioEnabled() } .start(mainThreadExecutor, captureListener) } -
If we need to stop recording, just stop it:
currentRecording!!.stop()
Read Barcodes and QR Codes from Video Files
Next, we can record a video containing a QR code and read QR codes from it.
Decode while Playing to Emulate Live Scan
We can use VideoView to play the video and then use PixelCopy to take a snapshot of the current frame for decoding. In this way, we can emulate live scan.
-
Play the video.
videoView.setVideoURI(uri) videoView.start() -
Use
PixelCopyto take a snapshot./** * Pixel copy to copy SurfaceView/VideoView into BitMap * source: https://stackoverflow.com/questions/27434087/how-to-capture-screenshot-or-video-frame-of-videoview-in-android */ @RequiresApi(Build.VERSION_CODES.N) fun usePixelCopy(videoView: SurfaceView, callback: (Bitmap?) -> Unit) { val bitmap: Bitmap = Bitmap.createBitmap( videoView.width, videoView.height, Bitmap.Config.ARGB_8888 ); try { // Create a handler thread to offload the processing of the image. val handlerThread = HandlerThread("PixelCopier"); handlerThread.start(); PixelCopy.request( videoView, bitmap, PixelCopy.OnPixelCopyFinishedListener { copyResult -> if (copyResult == PixelCopy.SUCCESS) { callback(bitmap) }else{ decoding = false } handlerThread.quitSafely(); }, Handler(handlerThread.looper) ) } catch (e: IllegalArgumentException) { callback(null) // PixelCopy may throw IllegalArgumentException, make sure to handle it e.printStackTrace() } } -
Start a timer to take snapshots and decode. A
decodingproperty is used to decide whether to decode based on the status of the previous task.@RequiresApi(Build.VERSION_CODES.N) private fun decodeVideo(){ val timer = Timer() timer.scheduleAtFixedRate(timerTask { try { if (videoView.isPlaying) { if (decoding == false) { decoding = true usePixelCopy(videoView){ bitmap: Bitmap? -> val bm = rotateBitmaptoFitScreen(bitmap!!) val textResults = decodeBitmap(bm) decoding = false } } } }catch (exc:Exception) { exc.printStackTrace() } },0,2) videoView.start() } -
Dynamsoft Barcode Reader is used to decode the bitmap.
private fun decodeBitmap(bm:Bitmap):ArrayList<String> { val results:ArrayList<String> = ArrayList<String>() val capturedResult = cvr.capture(bm, EnumPresetTemplate.PT_READ_BARCODES) val barcodesResult = capturedResult?.decodedBarcodesResult if (barcodesResult != null && barcodesResult.items != null) { for (item in barcodesResult.items) { results.add(item.text) } } return results }
PS: How to add Dynamsoft Barcode Reader to the project and initialize a CaptureVisionRouter instance.
-
Add the following to the project’s
build.gradle.allprojects { repositories { maven { url "https://download2.dynamsoft.com/maven/aar" } } } -
Add the following to the app’s
build.gradle.implementation 'com.dynamsoft:barcodereaderbundle:11.6.2000' -
In the
VideoView’s activity, create an instance ofCaptureVisionRouterand set up its settings for video decoding of EAN13 and QR codes.private lateinit var cvr: CaptureVisionRouter private fun initDBR(){ cvr = CaptureVisionRouter(this) try { val settings = cvr.getSimplifiedSettings(EnumPresetTemplate.PT_READ_BARCODES) val barcodeSettings = settings.barcodeSettings if (barcodeSettings != null) { barcodeSettings.barcodeFormatIds = EnumBarcodeFormat.BF_EAN_13 or EnumBarcodeFormat.BF_QR_CODE barcodeSettings.expectedBarcodesCount = 1 } cvr.updateSettings(EnumPresetTemplate.PT_READ_BARCODES, settings) } catch (e: Exception) { e.printStackTrace() } } -
Dynamsoft Barcode Reader requires a license to use. Initialize it in
MainActivitywith the key you obtained in the prerequisites:LicenseManager.initLicense( "LICENSE-KEY", this ) { isSuccessful, e -> if (!isSuccessful) { e?.printStackTrace() } }
Use FFmpeg to Grab Every Frame and Then Decode
We can also grab every video frame and then decode. Since Android does not provide a built-in API for this, we can use FFmpeg to do this.
-
Install javacv which provides a FFMpeg library for Android.
Add the following the the app’s
build.gradle:implementation group: 'org.bytedeco', name: 'javacv', version: "1.5.7" implementation group: 'org.bytedeco', name: 'ffmpeg', version: '5.0-1.5.7' implementation group: 'org.bytedeco', name: 'ffmpeg', version: '5.0-1.5.7', classifier: 'android-arm64' implementation group: 'org.bytedeco', name: 'ffmpeg', version: '5.0-1.5.7', classifier: 'android-x86_64'Here, we only add the required architectures.
-
Use
FFMpegFrameGrabberto grab every frame from video and decode. The video frame may be horizontal, which needs rotation if the video is shot with the phone in portrait mode.val inputStream: InputStream? = contentResolver.openInputStream(uri) val frameGrabber = FFmpegFrameGrabber(inputStream) frameGrabber.start() val totalFrames = frameGrabber.lengthInVideoFrames //val totalFrames = 2 val th = thread(start=true) { for (i in 0..totalFrames-1) { val frame = frameGrabber.grabFrame() var bm = AndroidFrameConverter().convert(frame) bm = rotateBitmaptoFitScreen(bm) decodeBitmap(bm) } frameGrabber.close() }
Reading Test
Let’s run a reading test on the following 5-second QR code video.
Result in video mode:
| Item | Statistics |
|---|---|
| First Video Position with Barcodes Found (ms) | 143 |
| Frames with Barcode Found/Frames Processed in Video Mode | 48/49 |
| First Barcode Result | dynamsoft |
Result in frame mode:
| Item | Statistics |
|---|---|
| First Video Frame Index with Barcodes Found | 1 |
| Frames with Barcode Found/Frames Processed in Frame Mode | 139/140 |
| First Barcode Result | dynamsoft |
We can see that Dynamsoft Barcode Reader has a good reading rate on this test video.
Common Issues & Edge Cases
- PixelCopy fails on some devices.
PixelCopyneeds a hardware-accelerated surface; if the copy fails, the sample resets thedecodingflag and retries on the next timer tick. On unsupported devices, use the FFmpeg frame mode instead. - The video frame is rotated. Videos recorded in portrait mode come back rotated. The sample rotates every grabbed or copied bitmap with
rotateBitmaptoFitScreen()before decoding, otherwise the barcode orientation can hurt the read rate. - Decoding every frame is slow. Frame-by-frame decoding with JavaCV converts and processes each frame, so expect lower throughput than live scanning. Use the stop conditions in the sample to bound long benchmark runs.
- No barcodes found in a test video. Keep the code sharp and roughly parallel to the camera plane when recording; blur and extreme perspective reduce the read rate in both decode modes.