Record a Video using CameraX and Read Barcodes from Video Files
Normally, we scan barcodes and QR codes from live camera streams or static images. But sometimes, we may need to read barcodes from video files, like scanning the barcodes on a long shelf and evaluating the performance of live scanning. A video contains rich information. A state-of-art mobile phone can shoot a 240 FPS high-speed video in 1920x1080 resolution. There are great potential reading barcodes and QR codes from videos.
In this article, we are going to create an Android app which can record videos using CameraX in Kotlin and read barcodes and QR codes from video files using Dynamsoft Barcode Reader.
Getting started with Dynamsoft Barcode Reader
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
Recorder
and 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
VideoCapture
use case using theRecorder
.videoCapture = VideoCapture.withOutput(recorder)
-
Add the
VideoCapture
use 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
PixelCopy
to 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
decoding
property 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 textResults = reader.decodeBufferedImage(bm) for (tr in textResults) { results.add(tr.barcodeText) } return results }
PS: How to add Dynamsoft Barcode Reader to the project and initialize a BarcodeReader
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:dynamsoftbarcodereader:9.6.40@aar'
-
In the
VideoView
’s activity, create an instance ofBarcodeReader
and set up its runtime settings for video decoding of EAN13 and QR codes.private lateinit var reader: BarcodeReader private fun initDBR(){ reader = BarcodeReader() reader.updateRuntimeSettings(EnumPresetTemplate.VIDEO_SINGLE_BARCODE) val settings = reader.runtimeSettings settings.barcodeFormatIds = EnumBarcodeFormat.BF_EAN_13 or EnumBarcodeFormat.BF_QR_CODE reader.updateRuntimeSettings(settings) }
-
Dynamsoft Barcode Reader requires a license to use. You can apply a license here and use it in the following way:
BarcodeReader.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==" ) { 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
FFMpegFrameGrabber
to 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.