Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat: Android Impeller support #1283

Draft
wants to merge 24 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ec7dd4b
require Flutter 3.27
navaronbracke Jan 6, 2025
b4ac6da
implement surface producer; modulo orientation change
navaronbracke Jan 6, 2025
9262a5c
update changelog
navaronbracke Jan 7, 2025
05b3850
add orientation correction function for Android
navaronbracke Jan 7, 2025
df5d782
add surface producer delegate
navaronbracke Jan 10, 2025
320f99a
set up delegate
navaronbracke Jan 10, 2025
1135ba6
add extension for device orientation parsing
navaronbracke Jan 10, 2025
f441b12
add from configuration constructor
navaronbracke Jan 10, 2025
e346344
format
navaronbracke Jan 16, 2025
02c1207
throw when parsing invalid orientation
navaronbracke Jan 17, 2025
2193261
let surface producer listen to orientation changes
navaronbracke Jan 17, 2025
d6108a1
pass additional parameters to method call result
navaronbracke Jan 20, 2025
6738314
use nullable current device orientation
navaronbracke Jan 20, 2025
db60787
implement device orientation listener
navaronbracke Jan 20, 2025
867ec49
add extension for device orientation
navaronbracke Jan 20, 2025
b03a0b9
let orientation listener observe orientation
navaronbracke Jan 20, 2025
673f65a
provide required args for start result
navaronbracke Jan 20, 2025
5952a89
dispose barcode handler stream handler
navaronbracke Jan 20, 2025
7582ac7
attach device orientation channel
navaronbracke Jan 20, 2025
6b08a6d
pass orientation events through sink
navaronbracke Jan 20, 2025
2c6c592
setup device orientation listener
navaronbracke Jan 20, 2025
98e4d55
disable torch for sample
navaronbracke Jan 20, 2025
3be2a4b
Merge branch 'develop' into android_impeller
navaronbracke Jan 24, 2025
a7f6b99
turn off rotation correction for now
navaronbracke Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## NEXT

* This release requires Flutter 3.27.0 or higher.

Improvements:
* [Android] Added support for Impeller.

## 7.0.0-beta.4

**BREAKING CHANGES:**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand
}
}

fun dispose() {
eventChannel.setStreamHandler(null)
}

override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) {
this.eventSink = eventSink
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package dev.steenbakker.mobile_scanner

import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.Display
import android.view.Surface
import android.view.WindowManager
import dev.steenbakker.mobile_scanner.utils.serialize
import io.flutter.embedding.engine.systemchannels.PlatformChannel
import io.flutter.plugin.common.EventChannel

/**
* This class will listen to device orientation changes.
*
* When a new orientation is received, the registered listener will be invoked.
*/
class DeviceOrientationListener(
private val activity: Activity,
): BroadcastReceiver(), EventChannel.StreamHandler {

companion object {
// The intent filter for listening to orientation changes.
private val orientationIntentFilter = IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)
}

// The event sink that handles device orientation events.
private var deviceOrientationEventSink: EventChannel.EventSink? = null

// The last received orientation. This is used to prevent duplicate events.
private var lastOrientation: PlatformChannel.DeviceOrientation? = null
// Whether the device orientation is currently being observed.
private var listening = false

override fun onReceive(context: Context?, intent: Intent?) {
val orientation: PlatformChannel.DeviceOrientation = getUIOrientation()
if (orientation != lastOrientation) {
Handler(Looper.getMainLooper()).post {
deviceOrientationEventSink?.success(orientation.serialize())
}
}

lastOrientation = orientation
}

override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) {
deviceOrientationEventSink = eventSink
}

override fun onCancel(event: Any?) {
deviceOrientationEventSink = null
}

@Suppress("deprecation")
private fun getDisplay(): Display {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activity.display!!
} else {
(activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
}
}

/**
* Gets the current user interface orientation.
*/
fun getUIOrientation(): PlatformChannel.DeviceOrientation {
val rotation: Int = getDisplay().rotation
val orientation: Int = activity.resources.configuration.orientation

return when(orientation) {
Configuration.ORIENTATION_PORTRAIT -> {
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
PlatformChannel.DeviceOrientation.PORTRAIT_UP
} else {
PlatformChannel.DeviceOrientation.PORTRAIT_DOWN
}
}
Configuration.ORIENTATION_LANDSCAPE -> {
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT
} else {
PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT
}
}
Configuration.ORIENTATION_UNDEFINED -> PlatformChannel.DeviceOrientation.PORTRAIT_UP
else -> PlatformChannel.DeviceOrientation.PORTRAIT_UP
}
}

/**
* Start listening to device orientation changes.
*/
fun start() {
if (listening) {
return
}

listening = true
activity.registerReceiver(this, orientationIntentFilter)

// Trigger the orientation listener with the current value.
onReceive(activity, null)
}

/**
* Stop listening to device orientation changes.
*/
fun stop() {
if (!listening) {
return
}

activity.unregisterReceiver(this)
listening = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.TorchState
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
Expand All @@ -37,20 +38,23 @@ import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import dev.steenbakker.mobile_scanner.utils.YuvToRgbConverter
import dev.steenbakker.mobile_scanner.utils.serialize
import io.flutter.view.TextureRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.concurrent.Executors
import kotlin.math.roundToInt

class MobileScanner(
private val activity: Activity,
private val textureRegistry: TextureRegistry,
private val mobileScannerCallback: MobileScannerCallback,
private val mobileScannerErrorCallback: MobileScannerErrorCallback,
private val deviceOrientationListener: DeviceOrientationListener,
private val barcodeScannerFactory: (options: BarcodeScannerOptions?) -> BarcodeScanner = ::defaultBarcodeScannerFactory,
) {

Expand All @@ -59,7 +63,7 @@ class MobileScanner(
private var camera: Camera? = null
private var cameraSelector: CameraSelector? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
private var surfaceProducer: TextureRegistry.SurfaceProducer? = null
private var scanner: BarcodeScanner? = null
private var lastScanned: List<String?>? = null
private var scannerTimeout = false
Expand Down Expand Up @@ -189,6 +193,70 @@ class MobileScanner(
}
}

/**
* Create a {@link Preview.SurfaceProvider} that specifies how to provide a {@link Surface} to a
* {@code Preview}.
*/
@VisibleForTesting
fun createSurfaceProvider(surfaceProducer: TextureRegistry.SurfaceProducer): Preview.SurfaceProvider {
return Preview.SurfaceProvider {
request: SurfaceRequest ->
run {
// Set the callback for the surfaceProducer to invalidate Surfaces that it produces
// when they get destroyed.
surfaceProducer.setCallback(
object : TextureRegistry.SurfaceProducer.Callback {
override fun onSurfaceAvailable() {
// Do nothing. The Preview.SurfaceProvider will handle this
// whenever a new Surface is needed.
}

// TODO: replace with "onSurfaceCleanup" when available in Flutter 3.28 or later
// See https://github.com/flutter/flutter/pull/160937
override fun onSurfaceDestroyed() {
// Invalidate the SurfaceRequest so that CameraX knows to to make a new request
// for a surface.
request.invalidate()
}
}
)

// Provide the surface.
surfaceProducer.setSize(request.resolution.width, request.resolution.height)

val surface: Surface = surfaceProducer.surface

// The single thread executor is only used to invoke the result callback.
// Thus it is safe to use a new executor,
// instead of reusing the executor that is passed to the camera process provider.
request.provideSurface(surface, Executors.newSingleThreadExecutor()) {
// Handle the result of the request for a surface.
// See: https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result

// Always attempt a release.
surface.release()
Copy link
Collaborator Author

@navaronbracke navaronbracke Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that adding this line fixes any occurrences of A resource failed to call close that we sometimes get.
Previously, we never did anything with the result callback, thus also not releasing the surface.


val resultCode: Int = it.resultCode

when(resultCode) {
SurfaceRequest.Result.RESULT_REQUEST_CANCELLED,
SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE,
SurfaceRequest.Result.RESULT_SURFACE_ALREADY_PROVIDED,
SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY -> {
// Only need to release, do nothing.
}
SurfaceRequest.Result.RESULT_INVALID_SURFACE -> {
// The surface was invalid, so it is not clear how to recover from this.
}
else -> {
// Fallthrough, in case any result codes are added later.
}
}
}
}
}
}

private fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
Expand Down Expand Up @@ -249,19 +317,22 @@ class MobileScanner(
this.returnImage = returnImage
this.invertImage = invertImage

if (camera?.cameraInfo != null && preview != null && textureEntry != null && !isPaused) {
if (camera?.cameraInfo != null && preview != null && surfaceProducer != null && !isPaused) {

// TODO: resume here for seamless transition
// TODO: resume here for seamless transition
// if (isPaused) {
// resumeCamera()
// mobileScannerStartedCallback(
// MobileScannerStartParameters(
// if (portrait) width else height,
// if (portrait) height else width,
// currentTorchState,
// textureEntry!!.id(),
// numberOfCameras ?: 0
// )
// MobileScannerStartParameters(
// if (portrait) width else height,
// if (portrait) height else width,
// deviceOrientationListener.getUIOrientation().serialize(),
// sensorRotationDegrees,
// surfaceProducer!!.handlesCropAndRotation(),
// currentTorchState,
// surfaceProducer!!.id(),
// numberOfCameras ?: 0
// )
// )
// return
// }
Expand All @@ -287,23 +358,10 @@ class MobileScanner(
}

cameraProvider?.unbindAll()
textureEntry = textureEntry ?: textureRegistry.createSurfaceTexture()
surfaceProducer = surfaceProducer ?: textureRegistry.createSurfaceProducer()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juliansteenbakker The ?: expression was introduced with the pause feature. Do you recall why we need it?
I am mostly migrating from TextureEntry to SurfaceProducer, so I'd rather not change anything else.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ?: was added because otherwise a new texture was created instead of the existing one being used after resuming from pause.

val surfaceProvider: Preview.SurfaceProvider = createSurfaceProvider(surfaceProducer!!)

// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
if (isStopped()) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer have this check, I'm not sure if we should put it back or not.

return@SurfaceProvider
}

val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(
Copy link
Collaborator Author

@navaronbracke navaronbracke Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is replaced by the surfaceProducer.setSize(request.resolution.width, request.resolution.height) on line 201.

request.resolution.width,
request.resolution.height
)

val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}

// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder()
Expand Down Expand Up @@ -384,7 +442,8 @@ class MobileScanner(
val resolution = analysis.resolutionInfo!!.resolution
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0
val sensorRotationDegrees = camera?.cameraInfo?.sensorRotationDegrees ?: 0
val portrait = sensorRotationDegrees % 180 == 0

// Start with 'unavailable' torch state.
var currentTorchState: Int = -1
Expand All @@ -397,12 +456,17 @@ class MobileScanner(
currentTorchState = it.torchState.value ?: -1
}

deviceOrientationListener.start()

mobileScannerStartedCallback(
MobileScannerStartParameters(
if (portrait) width else height,
if (portrait) height else width,
deviceOrientationListener.getUIOrientation().serialize(),
sensorRotationDegrees,
surfaceProducer!!.handlesCropAndRotation(),
currentTorchState,
textureEntry!!.id(),
surfaceProducer!!.id(),
numberOfCameras ?: 0
)
)
Expand All @@ -420,6 +484,7 @@ class MobileScanner(
throw AlreadyStopped()
}

deviceOrientationListener.stop()
pauseCamera()
}

Expand All @@ -431,6 +496,7 @@ class MobileScanner(
throw AlreadyStopped()
}

deviceOrientationListener.stop()
releaseCamera()
}

Expand Down Expand Up @@ -469,8 +535,9 @@ class MobileScanner(
// The camera will be closed when the last use case is unbound.
cameraProvider?.unbindAll()

textureEntry?.release()
textureEntry = null
// Release the surface for the preview.
surfaceProducer?.release()
surfaceProducer = null

// Release the scanner.
scanner?.close()
Expand Down
Loading
Loading