diff --git a/README.md b/README.md
index c715acf..4e4ca35 100644
--- a/README.md
+++ b/README.md
@@ -15,24 +15,23 @@ Supported Compose version:
| 1.6.x | 0.1.0+ |
| 1.7 | 0.2 - 0.3 |
| 1.8 | 0.4.0 |
-| 1.9 | 0.5.0 |
+| 1.9 | 0.5.0 - 0.6.x |
# Dependency
Add the dependency to your commonMain sourceSet (KMP) / Android dependencies (android only):
```kotlin
-implementation("io.github.kalinjul.easyqrscan:scanner:0.5.0")
+implementation("io.github.kalinjul.easyqrscan:scanner:0.6.0")
```
Or, for your libs.versions.toml:
```toml
[versions]
-easyqrscan = "0.5.0"
+easyqrscan = "0.6.0"
[libraries]
easyqrscan = { module = "io.github.kalinjul.easyqrscan:scanner", version.ref = "easyqrscan" }
```
-# Usage
-## Camera Permissions
+# Setup Camera Permissions
Include this at root level in your AndroidManifest.xml:
```xml
@@ -43,10 +42,18 @@ Include this at root level in your AndroidManifest.xml:
Add this key to the Info.plist in your xcode project:
```NSCameraUsageDescription``` and provide a description as value
-## Compose UI
+# Usage
+
+The scanner is included by calling a single composable function ```Scanner()``` or ```ScannerWithPermissions```:
+
```kotlin
// basic permission handling included:
-ScannerWithPermissions(onScanned = { println(it); true }, types = listOf(CodeType.QR))
+ScannerWithPermissions(
+ onScanned = { println(it); true }, // return true to disable the scanner, false to continue scanning
+ types = listOf(CodeType.QR),
+ cameraPosition = CameraPosition.BACK,
+ enableTorch = false // toggle this to enable/disable the flashlight
+)
// or, if you handle permissions yourself:
Scanner(onScanned = { println(it); true }, types = listOf(CodeType.QR))
diff --git a/camerautils/build.gradle.kts b/camerautils/build.gradle.kts
new file mode 100644
index 0000000..c696a01
--- /dev/null
+++ b/camerautils/build.gradle.kts
@@ -0,0 +1,35 @@
+import org.publicvalue.convention.config.configureIosTargets
+
+plugins {
+ id("org.publicvalue.convention.android.library")
+ id("org.publicvalue.convention.kotlin.multiplatform.mobile")
+ id("org.publicvalue.convention.centralPublish")
+ id("org.publicvalue.convention.compose.multiplatform")
+}
+
+description = "Compose Multiplatform Camera Utilities for Android/iOS"
+
+kotlin {
+ configureIosTargets()
+ jvm()
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ }
+ }
+
+ androidMain {
+ dependencies {
+ implementation(libs.androidx.camera.camera2)
+ }
+ }
+
+ jvmMain {
+ dependencies {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/camerautils/src/androidMain/kotlin/AndroidCameraUtils.kt b/camerautils/src/androidMain/kotlin/AndroidCameraUtils.kt
new file mode 100644
index 0000000..cbec403
--- /dev/null
+++ b/camerautils/src/androidMain/kotlin/AndroidCameraUtils.kt
@@ -0,0 +1,29 @@
+package org.publicvalue.multiplatform.qrcode
+
+import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import androidx.core.content.ContextCompat
+
+class AndroidCameraUtils(context: Context) : CameraUtils {
+ private val cameraManager = ContextCompat.getSystemService(context,CameraManager::class.java)
+
+ override fun setTorchMode(
+ cameraPosition: CameraPosition,
+ value: Boolean
+ ): Boolean {
+ val cameraId = cameraManager?.cameraIdList?.find {
+ cameraManager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == cameraPosition.toCharacteristics()
+ }
+ if (cameraId == null) {
+ return false
+ }
+ cameraId.let { cameraManager.setTorchMode(it, value) }
+ return true
+ }
+}
+
+fun CameraPosition.toCharacteristics() = when(this) {
+ CameraPosition.FRONT -> CameraCharacteristics.LENS_FACING_FRONT
+ CameraPosition.BACK -> CameraCharacteristics.LENS_FACING_BACK
+}
diff --git a/camerautils/src/androidMain/kotlin/CameraUtils.android.kt b/camerautils/src/androidMain/kotlin/CameraUtils.android.kt
new file mode 100644
index 0000000..5d17ec1
--- /dev/null
+++ b/camerautils/src/androidMain/kotlin/CameraUtils.android.kt
@@ -0,0 +1,13 @@
+package org.publicvalue.multiplatform.qrcode
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+actual fun rememberCameraUtils(): CameraUtils {
+ val context = LocalContext.current
+ return remember(context) {
+ AndroidCameraUtils(context)
+ }
+}
\ No newline at end of file
diff --git a/scanner/src/commonMain/kotlin/CameraPosition.kt b/camerautils/src/commonMain/kotlin/CameraPosition.kt
similarity index 100%
rename from scanner/src/commonMain/kotlin/CameraPosition.kt
rename to camerautils/src/commonMain/kotlin/CameraPosition.kt
diff --git a/camerautils/src/commonMain/kotlin/CameraUtils.kt b/camerautils/src/commonMain/kotlin/CameraUtils.kt
new file mode 100644
index 0000000..61ac44b
--- /dev/null
+++ b/camerautils/src/commonMain/kotlin/CameraUtils.kt
@@ -0,0 +1,10 @@
+package org.publicvalue.multiplatform.qrcode
+
+import androidx.compose.runtime.Composable
+
+interface CameraUtils {
+ fun setTorchMode(cameraPosition: CameraPosition, value: Boolean): Boolean
+}
+
+@Composable
+expect fun rememberCameraUtils(): CameraUtils
\ No newline at end of file
diff --git a/camerautils/src/iosMain/kotlin/AVCaptureDevice.getDevice.kt b/camerautils/src/iosMain/kotlin/AVCaptureDevice.getDevice.kt
new file mode 100644
index 0000000..1850b90
--- /dev/null
+++ b/camerautils/src/iosMain/kotlin/AVCaptureDevice.getDevice.kt
@@ -0,0 +1,21 @@
+package org.publicvalue.multiplatform.qrcode
+
+import platform.AVFoundation.AVCaptureDevice
+import platform.AVFoundation.AVCaptureDevicePositionBack
+import platform.AVFoundation.AVCaptureDevicePositionFront
+import platform.AVFoundation.AVMediaTypeVideo
+import platform.AVFoundation.position
+
+fun AVCaptureDevice.Companion.getDevice(cameraPosition: CameraPosition): AVCaptureDevice? {
+ val devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).map { it as AVCaptureDevice }
+
+ return devices.firstOrNull { device ->
+ when(cameraPosition) {
+ CameraPosition.FRONT -> device.position == AVCaptureDevicePositionFront
+ CameraPosition.BACK -> device.position == AVCaptureDevicePositionBack
+ }
+ } ?: run {
+ println("Could not find camera with position: $cameraPosition, using default camera")
+ AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
+ }
+}
\ No newline at end of file
diff --git a/camerautils/src/iosMain/kotlin/CameraUtils.ios.kt b/camerautils/src/iosMain/kotlin/CameraUtils.ios.kt
new file mode 100644
index 0000000..ee48a77
--- /dev/null
+++ b/camerautils/src/iosMain/kotlin/CameraUtils.ios.kt
@@ -0,0 +1,8 @@
+package org.publicvalue.multiplatform.qrcode
+
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun rememberCameraUtils(): CameraUtils {
+ return IosCameraUtils()
+}
\ No newline at end of file
diff --git a/camerautils/src/iosMain/kotlin/IosCameraUtils.kt b/camerautils/src/iosMain/kotlin/IosCameraUtils.kt
new file mode 100644
index 0000000..8dab646
--- /dev/null
+++ b/camerautils/src/iosMain/kotlin/IosCameraUtils.kt
@@ -0,0 +1,47 @@
+package org.publicvalue.multiplatform.qrcode
+
+import kotlinx.cinterop.BetaInteropApi
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.ObjCObjectVar
+import kotlinx.cinterop.alloc
+import kotlinx.cinterop.memScoped
+import kotlinx.cinterop.ptr
+import kotlinx.cinterop.value
+import platform.AVFoundation.AVCaptureDevice
+import platform.AVFoundation.AVCaptureTorchModeOff
+import platform.AVFoundation.AVCaptureTorchModeOn
+import platform.AVFoundation.hasTorch
+import platform.AVFoundation.setTorchMode
+import platform.Foundation.NSError
+
+class IosCameraUtils: CameraUtils {
+ @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
+ override fun setTorchMode(
+ cameraPosition: CameraPosition,
+ value: Boolean
+ ): Boolean {
+ val device = AVCaptureDevice.getDevice(cameraPosition)
+ if (device == null || !device.hasTorch()) {
+ return false
+ }
+
+ memScoped {
+ val error = alloc>()
+ device.lockForConfiguration(error.ptr)
+ if (error.value != null) {
+ throw Exception("Error locking device: ${error.value}")
+ }
+ }
+ device.setTorchMode(
+ if (value) {
+ AVCaptureTorchModeOn
+ } else {
+ AVCaptureTorchModeOff
+ }
+ )
+
+ device.unlockForConfiguration()
+ return true
+ }
+
+}
diff --git a/camerautils/src/jvmMain/kotlin/CameraUtils.jvm.kt b/camerautils/src/jvmMain/kotlin/CameraUtils.jvm.kt
new file mode 100644
index 0000000..95bf4f3
--- /dev/null
+++ b/camerautils/src/jvmMain/kotlin/CameraUtils.jvm.kt
@@ -0,0 +1,16 @@
+package org.publicvalue.multiplatform.qrcode
+
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun rememberCameraUtils(): CameraUtils {
+ return object : CameraUtils {
+ override fun setTorchMode(
+ cameraPosition: CameraPosition,
+ value: Boolean
+ ): Boolean {
+ println("CameraUtils not implemented on JVM")
+ return false
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-app/shared/src/commonMain/kotlin/MainView.kt b/sample-app/shared/src/commonMain/kotlin/MainView.kt
index 92e5e28..0c02f2b 100644
--- a/sample-app/shared/src/commonMain/kotlin/MainView.kt
+++ b/sample-app/shared/src/commonMain/kotlin/MainView.kt
@@ -1,12 +1,15 @@
package org.publicvalue.multiplatform.qrcode.sample
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -14,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Job
@@ -25,6 +29,7 @@ import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeMark
import kotlin.time.TimeSource
+import org.publicvalue.multiplatform.qrcode.rememberCameraUtils
@Composable
fun MainView() {
@@ -38,11 +43,36 @@ fun MainView() {
) {
Column(modifier = Modifier.padding(it)) {
Text("Scan QR-Code below")
- var scannerVisible by remember {mutableStateOf(false)}
- Button(onClick = {
- scannerVisible = !scannerVisible
- }) {
- Text("Toggle scanner (visible: $scannerVisible)")
+ var scannerVisible by remember { mutableStateOf(false) }
+ var enableTorch by remember { mutableStateOf(false) }
+
+ val cameraUtils = rememberCameraUtils()
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text("Toggle scanner")
+ Switch(
+ checked = scannerVisible,
+ onCheckedChange = {
+ scannerVisible = it
+ }
+ )
+ }
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text("Toggle torch")
+ Switch(
+ checked = enableTorch,
+ onCheckedChange = {
+ enableTorch = it
+ if (!scannerVisible) { // android needs separate handling when capturing video
+ cameraUtils.setTorchMode(CameraPosition.BACK, it)
+ }
+ }
+ )
+ }
}
var lastCode by remember { mutableStateOf(null)}
var snackbarJob by remember { mutableStateOf(null)}
@@ -68,7 +98,8 @@ fun MainView() {
false // continue scanning
},
types = listOf(CodeType.QR),
- cameraPosition = CameraPosition.BACK
+ cameraPosition = CameraPosition.BACK,
+ enableTorch = enableTorch,
)
}
}
diff --git a/scanner/build.gradle.kts b/scanner/build.gradle.kts
index 00b82c1..690e604 100644
--- a/scanner/build.gradle.kts
+++ b/scanner/build.gradle.kts
@@ -19,6 +19,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
+ api(projects.camerautils)
}
}
diff --git a/scanner/src/androidMain/kotlin/CameraView.kt b/scanner/src/androidMain/kotlin/CameraView.kt
new file mode 100644
index 0000000..c1c4bee
--- /dev/null
+++ b/scanner/src/androidMain/kotlin/CameraView.kt
@@ -0,0 +1,84 @@
+package org.publicvalue.multiplatform.qrcode
+
+import android.util.Log
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.compose.LocalLifecycleOwner
+
+@Composable
+fun CameraView(
+ modifier: Modifier = Modifier,
+ analyzer: BarcodeAnalyzer,
+ cameraPosition: CameraPosition,
+ enableTorch: Boolean,
+) {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val cameraProviderFuture = remember(context) {
+ ProcessCameraProvider.getInstance(context)
+ }
+
+ val preview = remember { Preview.Builder().build() }
+ val selector = remember(cameraPosition) {
+ CameraSelector.Builder()
+ .requireLensFacing(cameraPosition.toSelector())
+ .build()
+ }
+
+ val imageAnalysis = remember(context, analyzer) {
+ ImageAnalysis.Builder()
+ .build()
+ .also { it.setAnalyzer( ContextCompat.getMainExecutor(context), analyzer ) }
+ }
+
+ val camera = remember(cameraProviderFuture, imageAnalysis) {
+ runCatching {
+ cameraProviderFuture.get().unbindAll()
+ val camera1 = cameraProviderFuture.get().bindToLifecycle(
+ lifecycleOwner,
+ selector,
+ preview,
+ imageAnalysis
+ )
+ camera1
+ }.onFailure {
+ Log.e("CAMERA", "Camera bind error ${it.localizedMessage}", it)
+ }
+ }
+
+ LaunchedEffect(camera, enableTorch) {
+ camera.getOrNull()?.cameraControl?.enableTorch(enableTorch)
+ }
+
+ DisposableEffect(cameraProviderFuture) {
+ onDispose {
+ cameraProviderFuture.get().unbindAll()
+ }
+ }
+
+ AndroidView(
+ modifier = modifier.fillMaxSize(),
+ factory = { context ->
+ val previewView = PreviewView(context)
+ preview.setSurfaceProvider(previewView.surfaceProvider)
+ previewView
+ },
+ )
+}
+
+fun CameraPosition.toSelector() = when(this) {
+ CameraPosition.FRONT -> CameraSelector.LENS_FACING_FRONT
+ CameraPosition.BACK -> CameraSelector.LENS_FACING_BACK
+}
\ No newline at end of file
diff --git a/scanner/src/androidMain/kotlin/Scanner.android.kt b/scanner/src/androidMain/kotlin/Scanner.android.kt
index 2bbab8f..a0d3393 100644
--- a/scanner/src/androidMain/kotlin/Scanner.android.kt
+++ b/scanner/src/androidMain/kotlin/Scanner.android.kt
@@ -20,11 +20,12 @@ actual fun Scanner(
onScanned: (String) -> Boolean,
types: List,
cameraPosition: CameraPosition,
+ enableTorch: Boolean,
) {
val analyzer = remember() {
BarcodeAnalyzer(types.toFormat(), onScanned)
}
- CameraView(modifier, analyzer, cameraPosition)
+ CameraView(modifier, analyzer, cameraPosition, enableTorch)
}
@OptIn(ExperimentalPermissionsApi::class)
diff --git a/scanner/src/androidMain/kotlin/ScannerView.kt b/scanner/src/androidMain/kotlin/ScannerView.kt
deleted file mode 100644
index 9b5c682..0000000
--- a/scanner/src/androidMain/kotlin/ScannerView.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.publicvalue.multiplatform.qrcode
-
-import android.util.Log
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.Preview
-import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.view.PreviewView
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.compose.LocalLifecycleOwner
-
-@Composable
-fun CameraView(
- modifier: Modifier = Modifier,
- analyzer: BarcodeAnalyzer,
- cameraPosition: CameraPosition
-) {
- val localContext = LocalContext.current
- val lifecycleOwner = LocalLifecycleOwner.current
- val cameraProviderFuture = remember {
- ProcessCameraProvider.getInstance(localContext)
- }
- AndroidView(
- modifier = modifier.fillMaxSize(),
- factory = { context ->
- val previewView = PreviewView(context)
- val preview = Preview.Builder().build()
- val selector = CameraSelector.Builder()
- .let {
- when (cameraPosition) {
- CameraPosition.FRONT -> it.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
- CameraPosition.BACK -> it.requireLensFacing(CameraSelector.LENS_FACING_BACK)
- }
- }
- .build()
-
- preview.setSurfaceProvider(previewView.surfaceProvider)
-
- val imageAnalysis = ImageAnalysis.Builder().build()
- imageAnalysis.setAnalyzer(
- ContextCompat.getMainExecutor(context),
- analyzer
- )
-
- runCatching {
- cameraProviderFuture.get().unbindAll()
- cameraProviderFuture.get().bindToLifecycle(
- lifecycleOwner,
- selector,
- preview,
- imageAnalysis
- )
- }.onFailure {
- Log.e("CAMERA", "Camera bind error ${it.localizedMessage}", it)
- }
- previewView
- }
- )
-}
\ No newline at end of file
diff --git a/scanner/src/commonMain/kotlin/Scanner.kt b/scanner/src/commonMain/kotlin/Scanner.kt
index ca0ad75..69d4268 100644
--- a/scanner/src/commonMain/kotlin/Scanner.kt
+++ b/scanner/src/commonMain/kotlin/Scanner.kt
@@ -24,7 +24,8 @@ expect fun Scanner(
modifier: Modifier = Modifier,
onScanned: (String) -> Boolean,
types: List,
- cameraPosition: CameraPosition = CameraPosition.BACK
+ cameraPosition: CameraPosition = CameraPosition.BACK,
+ enableTorch: Boolean,
)
/**
@@ -43,6 +44,7 @@ fun ScannerWithPermissions(
onScanned: (String) -> Boolean,
types: List,
cameraPosition: CameraPosition = CameraPosition.BACK,
+ enableTorch: Boolean,
permissionText: String = "Camera is required for QR Code scanning",
openSettingsLabel: String = "Open Settings",
) {
@@ -51,6 +53,7 @@ fun ScannerWithPermissions(
onScanned = onScanned,
types = types,
cameraPosition = cameraPosition,
+ enableTorch = enableTorch,
permissionDeniedContent = { permissionState ->
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Text(
@@ -80,6 +83,7 @@ fun ScannerWithPermissions(
onScanned: (String) -> Boolean,
types: List,
cameraPosition: CameraPosition,
+ enableTorch: Boolean,
permissionDeniedContent: @Composable (CameraPermissionState) -> Unit,
) {
val permissionState = rememberCameraPermissionState()
@@ -91,7 +95,7 @@ fun ScannerWithPermissions(
}
if (permissionState.status == CameraPermissionStatus.Granted) {
- Scanner(modifier, types = types, onScanned = onScanned, cameraPosition = cameraPosition)
+ Scanner(modifier, types = types, onScanned = onScanned, cameraPosition = cameraPosition, enableTorch = enableTorch)
} else {
permissionDeniedContent(permissionState)
}
diff --git a/scanner/src/iosMain/kotlin/Scanner.ios.kt b/scanner/src/iosMain/kotlin/Scanner.ios.kt
index 9d2398d..46ae491 100644
--- a/scanner/src/iosMain/kotlin/Scanner.ios.kt
+++ b/scanner/src/iosMain/kotlin/Scanner.ios.kt
@@ -1,6 +1,8 @@
package org.publicvalue.multiplatform.qrcode
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -20,15 +22,32 @@ actual fun Scanner(
modifier: Modifier,
onScanned: (String) -> Boolean, // return true to abort scanning
types: List,
- cameraPosition: CameraPosition
+ cameraPosition: CameraPosition,
+ enableTorch: Boolean,
) {
+ var started by remember { mutableStateOf(false) }
+ val cameraUtils = rememberCameraUtils()
+ LaunchedEffect(enableTorch, started) {
+ if (started) {
+ cameraUtils.setTorchMode(cameraPosition, enableTorch)
+ }
+ }
+ DisposableEffect(Unit) {
+ onDispose {
+ cameraUtils.setTorchMode(cameraPosition, false)
+ }
+ }
UiScannerView(
modifier = modifier,
onScanned = {
onScanned(it)
},
allowedMetadataTypes = types.toFormat(),
- cameraPosition = cameraPosition
+ cameraPosition = cameraPosition,
+ onStarted = {
+ cameraUtils.setTorchMode(cameraPosition, enableTorch)
+ started = true
+ }
)
}
diff --git a/scanner/src/iosMain/kotlin/ScannerView.kt b/scanner/src/iosMain/kotlin/ScannerView.kt
index 8459e41..7c9fb14 100644
--- a/scanner/src/iosMain/kotlin/ScannerView.kt
+++ b/scanner/src/iosMain/kotlin/ScannerView.kt
@@ -15,15 +15,12 @@ import kotlinx.cinterop.alloc
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
-import kotlinx.cinterop.useContents
import kotlinx.cinterop.value
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import platform.AVFoundation.AVCaptureDevice
import platform.AVFoundation.AVCaptureDeviceInput
-import platform.AVFoundation.AVCaptureDevicePositionBack
-import platform.AVFoundation.AVCaptureDevicePositionFront
import platform.AVFoundation.AVCaptureMetadataOutput
import platform.AVFoundation.AVCaptureMetadataOutputObjectsDelegateProtocol
import platform.AVFoundation.AVCaptureSession
@@ -33,10 +30,8 @@ import platform.AVFoundation.AVCaptureVideoOrientationPortrait
import platform.AVFoundation.AVCaptureVideoOrientationPortraitUpsideDown
import platform.AVFoundation.AVCaptureVideoPreviewLayer
import platform.AVFoundation.AVLayerVideoGravityResizeAspectFill
-import platform.AVFoundation.AVMediaTypeVideo
import platform.AVFoundation.AVMetadataMachineReadableCodeObject
import platform.AVFoundation.AVMetadataObjectType
-import platform.AVFoundation.position
import platform.CoreGraphics.CGRect
import platform.CoreGraphics.CGRectZero
import platform.Foundation.NSError
@@ -55,12 +50,14 @@ fun UiScannerView(
// https://developer.apple.com/documentation/avfoundation/avmetadataobjecttype?language=objc
allowedMetadataTypes: List,
cameraPosition: CameraPosition,
- onScanned: (String) -> Boolean
+ onScanned: (String) -> Boolean,
+ onStarted: () -> Unit,
) {
val coordinator = remember {
ScannerCameraCoordinator(
onScanned = onScanned,
- cameraPosition = cameraPosition
+ cameraPosition = cameraPosition,
+ onStarted = onStarted
)
}
@@ -73,6 +70,8 @@ fun UiScannerView(
onDispose {
listener.unregister()
+ // stop capture
+ coordinator.captureSession.stopRunning()
}
}
@@ -80,7 +79,6 @@ fun UiScannerView(
modifier = modifier.fillMaxSize(),
factory = {
val previewContainer = ScannerPreviewView(coordinator)
- println("Calling prepare")
coordinator.prepare(previewContainer.layer, allowedMetadataTypes)
previewContainer
},
@@ -89,14 +87,6 @@ fun UiScannerView(
isNativeAccessibilityEnabled = true,
)
)
-
-// DisposableEffect(Unit) {
-// onDispose {
-// // stop capture
-// coordinator.
-// }
-// }
-
}
@OptIn(ExperimentalForeignApi::class)
@@ -116,6 +106,7 @@ class ScannerPreviewView(private val coordinator: ScannerCameraCoordinator): UIV
@OptIn(ExperimentalForeignApi::class)
class ScannerCameraCoordinator(
val onScanned: (String) -> Boolean,
+ val onStarted: () -> Unit,
val cameraPosition: CameraPosition
): AVCaptureMetadataOutputObjectsDelegateProtocol, NSObject() {
@@ -125,24 +116,13 @@ class ScannerCameraCoordinator(
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
fun prepare(layer: CALayer, allowedMetadataTypes: List) {
captureSession = AVCaptureSession()
- val devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).map { it as AVCaptureDevice }
-
- val device = devices.firstOrNull { device ->
- when(cameraPosition) {
- CameraPosition.FRONT -> device.position == AVCaptureDevicePositionFront
- CameraPosition.BACK -> device.position == AVCaptureDevicePositionBack
- }
- } ?: run {
- println("Could not find camera with position: $cameraPosition, using default camera")
- AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
- }
+ val device = AVCaptureDevice.getDevice(cameraPosition)
if (device == null) {
println("Device has no camera")
return
}
- println("Initializing video input")
val videoInput = memScoped {
val error: ObjCObjectVar = alloc>()
val videoInput = AVCaptureDeviceInput(device = device, error = error.ptr)
@@ -154,7 +134,6 @@ class ScannerCameraCoordinator(
}
}
- println("Adding video input")
if (videoInput != null && captureSession.canAddInput(videoInput)) {
captureSession.addInput(videoInput)
} else {
@@ -164,7 +143,6 @@ class ScannerCameraCoordinator(
val metadataOutput = AVCaptureMetadataOutput()
- println("Adding metadata output")
if (captureSession.canAddOutput(metadataOutput)) {
captureSession.addOutput(metadataOutput)
@@ -174,26 +152,16 @@ class ScannerCameraCoordinator(
println("Could not add output")
return
}
- println("Adding preview layer")
previewLayer = AVCaptureVideoPreviewLayer(session = captureSession).also {
it.frame = layer.bounds
it.videoGravity = AVLayerVideoGravityResizeAspectFill
- println("Set orientation")
setCurrentOrientation(newOrientation = UIDevice.currentDevice.orientation)
- println("Adding sublayer")
- layer.bounds.useContents {
- println("Bounds: ${this.size.width}x${this.size.height}")
-
- }
- layer.frame.useContents {
- println("Frame: ${this.size.width}x${this.size.height}")
- }
layer.addSublayer(it)
}
- println("Launching capture session")
GlobalScope.launch(Dispatchers.Default) {
captureSession.startRunning()
+ onStarted()
}
}
diff --git a/scanner/src/jvmMain/kotlin/Scanner.jvm.kt b/scanner/src/jvmMain/kotlin/Scanner.jvm.kt
index 67035d1..04d9d42 100644
--- a/scanner/src/jvmMain/kotlin/Scanner.jvm.kt
+++ b/scanner/src/jvmMain/kotlin/Scanner.jvm.kt
@@ -9,7 +9,8 @@ actual fun Scanner(
modifier: Modifier,
onScanned: (String) -> Boolean,
types: List,
- cameraPosition: CameraPosition
+ cameraPosition: CameraPosition,
+ enableTorch: Boolean,
) {
Text("Scanner not implemented for JVM")
}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8f6133e..91fe53c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -20,6 +20,7 @@ dependencyResolutionManagement {
rootProject.name="EasyQRScan"
include(":scanner")
+include(":camerautils")
include(":sample-app:shared")
include(":sample-app:android-app")