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

Fix ios rapid capture #42

Merged
merged 5 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion ImageSaverPlugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ kotlin {

android {
namespace = "com.kashif.image_saver_plugin"
compileSdk = 34
compileSdk = 35

defaultConfig {
minSdk = 21
Expand Down
2 changes: 1 addition & 1 deletion Sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ kotlin {

android {
namespace = "org.company.app"
compileSdk = 34
compileSdk = 35

defaultConfig {
minSdk = 21
Expand Down
44 changes: 4 additions & 40 deletions Sample/src/commonMain/kotlin/org/company/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,30 +92,8 @@ fun App() = AppTheme {
) {
val cameraPermissionState = remember { mutableStateOf(permissions.hasCameraPermission()) }
val storagePermissionState = remember { mutableStateOf(permissions.hasStoragePermission()) }
val qrScannerPlugin = rememberQRScannerPlugin(coroutineScope = coroutineScope)
val ocrPlugin = rememberOcrPlugin()

LaunchedEffect(Unit) {
qrScannerPlugin.getQrCodeFlow().distinctUntilChanged()
.collectLatest { qrCode ->
snackbarHostState.showSnackbar(
message = "QR Code Detected: $qrCode",
duration = SnackbarDuration.Short
)
qrScannerPlugin.pauseScanning()
}
}
LaunchedEffect(Unit) {
ocrPlugin.ocrFlow.consumeAsFlow().distinctUntilChanged()
.collectLatest { text ->
println("Text Detected: $text")
snackbarHostState.showSnackbar(
message = "Text Detected: $text",
duration = SnackbarDuration.Short
)
ocrPlugin.stopRecognition()
}
}



val cameraController = remember { mutableStateOf<CameraController?>(null) }
val imageSaverPlugin = rememberImageSaverPlugin(
Expand All @@ -139,8 +117,6 @@ fun App() = AppTheme {
CameraContent(
cameraController = cameraController,
imageSaverPlugin = imageSaverPlugin,
qrScannerPlugin = qrScannerPlugin,
ocrPlugin = ocrPlugin
)
}
}
Expand Down Expand Up @@ -171,8 +147,6 @@ private fun PermissionsHandler(
private fun CameraContent(
cameraController: MutableState<CameraController?>,
imageSaverPlugin: ImageSaverPlugin,
qrScannerPlugin: QRScannerPlugin,
ocrPlugin: OcrPlugin
) {
Box(modifier = Modifier.fillMaxSize()) {
CameraPreview(
Expand All @@ -184,26 +158,18 @@ private fun CameraContent(
setDirectory(Directory.PICTURES)
setTorchMode(TorchMode.OFF)
addPlugin(imageSaverPlugin)
addPlugin(qrScannerPlugin)
addPlugin(ocrPlugin)
},
onCameraControllerReady = {
print("==> Camera Controller Ready")
cameraController.value = it

}
)

cameraController.value?.let { controller ->
LaunchedEffect(controller) {
qrScannerPlugin.startScanning()
}
LaunchedEffect(controller) {
ocrPlugin.startRecognition()
}
EnhancedCameraScreen(
cameraController = controller,
imageSaverPlugin = imageSaverPlugin,
ocrPlugin
)
}
}
Expand All @@ -213,7 +179,6 @@ private fun CameraContent(
fun EnhancedCameraScreen(
cameraController: CameraController,
imageSaverPlugin: ImageSaverPlugin,
ocrPlugin: OcrPlugin
) {
val scope = rememberCoroutineScope()
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
Expand Down Expand Up @@ -246,7 +211,6 @@ fun EnhancedCameraScreen(
imageSaverPlugin = imageSaverPlugin,
onImageCaptured = {
imageBitmap = it
ocrPlugin.extractTextFromBitmap(it)
}
)
}
Expand Down Expand Up @@ -431,7 +395,7 @@ private suspend fun handleImageCapture(
when (val result = cameraController.takePicture()) {
is ImageCaptureResult.Success -> {
val bitmap = result.byteArray.decodeToImageBitmap()
onImageCaptured(bitmap)
// onImageCaptured(bitmap)

if (!imageSaverPlugin.config.isAutoSave) {
val customName = "Manual_${Uuid.random().toHexString()}"
Expand Down
5 changes: 3 additions & 2 deletions cameraK/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
alias(libs.plugins.android.library)
id("org.jetbrains.compose")
alias(libs.plugins.compose.compiler)
id("com.vanniktech.maven.publish") version "0.28.0"
id("com.vanniktech.maven.publish") version "0.30.0"
}

group = "com.kashif.camera_compose"
Expand Down Expand Up @@ -45,6 +45,7 @@ kotlin {
api(compose.foundation)
api(libs.coil3.compose)
api(libs.coil3.ktor)
api(libs.atomicfu)
}

commonTest.dependencies {
Expand Down Expand Up @@ -76,7 +77,7 @@ kotlin {

android {
namespace = "com.kashif.cameraK"
compileSdk = 34
compileSdk = 35

defaultConfig {
minSdk = 21
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package com.kashif.cameraK.capture

import com.kashif.cameraK.utils.MemoryManager
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.max

/**
* Manager for handling burst mode captures efficiently on Android
* Optimizes for rapid sequential photo captures with minimal delay
* Implements throttling and quality adaptation based on system load
*/
class BurstCaptureManager {
private val capturesInProgress = AtomicInteger(0)
private val pendingCaptures = AtomicInteger(0)

private val maxParallelCaptures = 2
private val maxTotalCaptures = 8
private val minCaptureIntervalMs = 300L

private var lastCaptureTime = atomic(0L)

private val processingExecutor = Executors.newFixedThreadPool(2)
private val burstModeActive = atomic(false)
private val lock = ReentrantLock()
private val coroutineScope = CoroutineScope(Dispatchers.Default)

private val captureQuality = atomic(95)
private val pendingCaptureQueue = ConcurrentLinkedQueue<CaptureRequest>()

init {

startQueueConsumer()
}

private data class CaptureRequest(
val captureFunction: () -> Unit,
val onComplete: () -> Unit
)

/**
* Request a capture
* @param captureFunction Function to call to initiate capture
* @param onComplete Callback when capture is complete
* @return true if capture was initiated or queued, false if rejected
*/
fun requestCapture(
captureFunction: () -> Unit,
onComplete: () -> Unit
): Boolean {
lock.withLock {
val totalPending = pendingCaptures.get() + capturesInProgress.get()

if (totalPending >= maxTotalCaptures) {
return false
}

val currentTime = System.currentTimeMillis()
val timeSinceLastCapture = currentTime - lastCaptureTime.value


pendingCaptures.incrementAndGet()
pendingCaptureQueue.add(CaptureRequest(captureFunction, onComplete))


if (pendingCaptures.get() > 2) {
burstModeActive.value = true
updateCaptureQuality()
}


lastCaptureTime.value = currentTime

return true
}
}

/**
* Start the queue consumer to process pending captures
*/
private fun startQueueConsumer() {
coroutineScope.launch {
while (true) {
processPendingCaptures()
delay(50)
}
}
}

/**
* Process any pending captures based on current state
*/
private fun processPendingCaptures() {

if (capturesInProgress.get() >= maxParallelCaptures) {
return
}

val currentTime = System.currentTimeMillis()
val timeSinceLastStart = currentTime - lastCaptureTime.value


if (timeSinceLastStart < minCaptureIntervalMs && capturesInProgress.get() > 0) {
return
}


val request = pendingCaptureQueue.poll() ?: return


capturesInProgress.incrementAndGet()
lastCaptureTime.value = currentTime


processingExecutor.execute {
try {

MemoryManager.updateMemoryStatus()


if (MemoryManager.isUnderMemoryPressure()) {
MemoryManager.clearBufferPools()
}


request.captureFunction()
} finally {
completeCapture(request.onComplete)
}
}
}

/**
* Complete a capture and process next in queue
*/
private fun completeCapture(onComplete: () -> Unit) {
capturesInProgress.decrementAndGet()


val remaining = pendingCaptures.decrementAndGet()


if (remaining <= 0) {
pendingCaptures.set(0)
burstModeActive.value = false
captureQuality.value = 95
}


onComplete()
}

/**
* Update quality settings based on current state
*/
private fun updateCaptureQuality() {
val currentPending = pendingCaptures.get()
val currentActive = capturesInProgress.get()
val total = currentPending + currentActive

captureQuality.value = when {
MemoryManager.isUnderMemoryPressure() -> 65
total > 5 -> 70
total > 3 -> 80
burstModeActive.value -> 85
else -> 95
}
}

/**
* Check if burst mode is currently active
*/
fun isBurstModeActive(): Boolean = burstModeActive.value

/**
* Get optimal quality setting based on current capture state and memory conditions
*/
fun getOptimalQuality(): Int {
updateCaptureQuality()
return captureQuality.value
}

/**
* Reset all capture state
*/
fun reset() {
lock.withLock {
pendingCaptureQueue.clear()
capturesInProgress.set(0)
pendingCaptures.set(0)
burstModeActive.value = false
lastCaptureTime.value = 0
captureQuality.value = 95
}
}

/**
* Clean up resources when no longer needed
*/
fun shutdown() {
processingExecutor.shutdown()
}
}
Loading