Skip to content

Commit 8bf6682

Browse files
committed
fix(android): release one-shot camera capture promptly
1 parent 41200c3 commit 8bf6682

1 file changed

Lines changed: 56 additions & 55 deletions

File tree

apps/android/app/src/main/java/ai/openclaw/app/node/CameraCaptureManager.kt

Lines changed: 56 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -134,66 +134,67 @@ class CameraCaptureManager(
134134
val selector = resolveCameraSelector(provider, facing, deviceId)
135135

136136
provider.unbindAll()
137-
try {
138-
// Bind only the still capture use case for this invocation, then release
139-
// it immediately so the camera is not left active after a one-shot snap.
140-
provider.bindToLifecycle(owner, selector, capture)
141-
142-
val (bytes, orientation) = capture.takeJpegWithExif(context.mainExecutor(), context.cacheDir)
143-
val decoded =
144-
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
145-
?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image")
146-
val rotated = rotateBitmapByExif(decoded, orientation)
147-
val scaled =
148-
if (maxWidth > 0 && rotated.width > maxWidth) {
149-
val h =
150-
(rotated.height.toDouble() * (maxWidth.toDouble() / rotated.width.toDouble()))
151-
.toInt()
152-
.coerceAtLeast(1)
153-
val s = rotated.scale(maxWidth, h)
154-
if (s !== rotated) rotated.recycle()
155-
s
156-
} else {
157-
rotated
158-
}
137+
// Bind only the still capture use case; CameraX owns camera open/close through the lifecycle owner.
138+
provider.bindToLifecycle(owner, selector, capture)
159139

140+
val (bytes, orientation) =
160141
try {
161-
val maxPayloadBytes = 5 * 1024 * 1024
162-
// Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit).
163-
val maxEncodedBytes = (maxPayloadBytes / 4) * 3
164-
val result =
165-
JpegSizeLimiter.compressToLimit(
166-
initialWidth = scaled.width,
167-
initialHeight = scaled.height,
168-
startQuality = (quality * 100.0).roundToInt().coerceIn(10, 100),
169-
maxBytes = maxEncodedBytes,
170-
encode = { width, height, q ->
171-
val bitmap =
172-
if (width == scaled.width && height == scaled.height) {
173-
scaled
174-
} else {
175-
scaled.scale(width, height)
176-
}
177-
val out = ByteArrayOutputStream()
178-
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, q, out)) {
179-
if (bitmap !== scaled) bitmap.recycle()
180-
throw IllegalStateException("UNAVAILABLE: failed to encode JPEG")
181-
}
182-
if (bitmap !== scaled) {
183-
bitmap.recycle()
184-
}
185-
out.toByteArray()
186-
},
187-
)
188-
val base64 = Base64.encodeToString(result.bytes, Base64.NO_WRAP)
189-
Payload(
190-
"""{"format":"jpg","base64":"$base64","width":${result.width},"height":${result.height}}""",
191-
)
142+
capture.takeJpegWithExif(context.mainExecutor(), context.cacheDir)
192143
} finally {
193-
scaled.recycle()
144+
// The JPEG bytes are self-contained; release CameraX before decoding and recompressing them.
145+
provider.unbind(capture)
146+
}
147+
val decoded =
148+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
149+
?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image")
150+
val rotated = rotateBitmapByExif(decoded, orientation)
151+
val scaled =
152+
if (maxWidth > 0 && rotated.width > maxWidth) {
153+
val h =
154+
(rotated.height.toDouble() * (maxWidth.toDouble() / rotated.width.toDouble()))
155+
.toInt()
156+
.coerceAtLeast(1)
157+
val s = rotated.scale(maxWidth, h)
158+
if (s !== rotated) rotated.recycle()
159+
s
160+
} else {
161+
rotated
194162
}
163+
164+
try {
165+
val maxPayloadBytes = 5 * 1024 * 1024
166+
// Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit).
167+
val maxEncodedBytes = (maxPayloadBytes / 4) * 3
168+
val result =
169+
JpegSizeLimiter.compressToLimit(
170+
initialWidth = scaled.width,
171+
initialHeight = scaled.height,
172+
startQuality = (quality * 100.0).roundToInt().coerceIn(10, 100),
173+
maxBytes = maxEncodedBytes,
174+
encode = { width, height, q ->
175+
val bitmap =
176+
if (width == scaled.width && height == scaled.height) {
177+
scaled
178+
} else {
179+
scaled.scale(width, height)
180+
}
181+
val out = ByteArrayOutputStream()
182+
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, q, out)) {
183+
if (bitmap !== scaled) bitmap.recycle()
184+
throw IllegalStateException("UNAVAILABLE: failed to encode JPEG")
185+
}
186+
if (bitmap !== scaled) {
187+
bitmap.recycle()
188+
}
189+
out.toByteArray()
190+
},
191+
)
192+
val base64 = Base64.encodeToString(result.bytes, Base64.NO_WRAP)
193+
Payload(
194+
"""{"format":"jpg","base64":"$base64","width":${result.width},"height":${result.height}}""",
195+
)
195196
} finally {
196-
provider.unbind(capture)
197+
scaled.recycle()
197198
}
198199
}
199200

0 commit comments

Comments
 (0)