Camposer
repository·main·Indexed 43 days ago
https://github.com/ujizin/camposerA Compose Multiplatform library for camera functionality, enabling photo capture, video recording, and hardware control (flash, zoom, focus) across Android and iOS. It provides a unified API using CameraPreview, CameraController, and CameraSession, with an optional camposer-code-scanner module for QR code scanning using ML Kit on Android and the Vision framework on iOS.
What's inside Camposer
- Camposer is a Compose Multiplatform camera library designed for modern camera stacks on Android and iOS. It provides high-level APIs for common camera functionalities including photo capture, video recording, flash/torch controls, zoom, focus, stabilization, and image analyzers.
Camposer Android Stack Overview
mainThe Camposer Android sample demonstrates an implementation using the following technology stack:
- Jetpack Compose: For UI development.
- Camposer: The core camera library.
- Jetpack Navigation (Compose): For handling app navigation.
- Coil: For image loading.
- Jetpack Compose Permissions (Google Accompanist): For managing runtime permissions required for camera access.
Capture and Recording workflows
mainCamposer handles still captures and video recording through the
CameraControllerusing platform-specific implementations of commands and controllers.Still Capture (
takePicture)When
CameraController.takePicture(executor, callback)is called:- It executes a
TakePictureCommand. - Android: Uses
ImageCapture.takePicture()via CameraX. - iOS: Uses
AVCapturePhotoOutput.capturePhoto(). - Results are returned via
CaptureResult.Success(uri)orCaptureResult.Failure(e)to the provided callback on the caller's executor/dispatcher.
Video Recording
- Starting:
CameraController.startRecording(...)triggers the platform'sRecordController. On Android, this usesVideoCapture.output.prepareRecording(); on iOS, it uses anAVAssetWriterpipeline. TheCameraSessionupdates itsisRecordingflag totrue. - Stopping:
CameraController.stopRecording()finalizes the recording, delivers theCaptureResult, and setsisRecordingtofalsein theCameraSession.
- It executes a
How Camposer's public API works
mainCamposer provides a unified Compose API for camera access in Kotlin Multiplatform (KMP) apps. The architecture separates user-facing controls from platform-specific hardware implementations (Android's CameraX and iOS's AVFoundation).
Core Components
CameraPreview: The root@Composablefunction. It renders the camera feed and manages the UI lifecycle.CameraSession: The primary state holder, created usingrememberCameraSession(). It is anexpectclass that provides access to the session's state, hardware info, and controller.CameraState: A collection of configurable camera properties exposed asMutableStateFlow(e.g.,FlashMode,zoom,captureMode,format). Mutating these flows automatically triggers updates in the underlying camera engine.CameraController: The user-facing API for triggering captures, includingtakePicture,startRecording, andstopRecording.CameraInfo: Provides runtime information about the hardware's capabilities, such as supported features and zoom/exposure ranges.
// Conceptual usage pattern @Composable fun MyCameraScreen() { val session = rememberCameraSession() Column { CameraPreview(session) Button(onClick = { session.controller.takePicture(executor, callback) }) { Text("Take Picture") } } }Understand cross-cutting concerns in Camposer
mainCamposer is built using several core architectural patterns that affect how you extend or interact with the library:
- KMP expect/actual: The core camera components (
CameraSession,CameraEngineImpl) and test fakes (FakeCameraEngine,FakeCameraTest,FakeCameraSession) use Kotlin Multiplatform'sexpect/actualmechanism. If you need to add a method to these components, you must update the commonexpectdeclaration and provide the correspondingactualimplementations for all supported platforms. - Coroutines for Async State: All asynchronous state is driven by Coroutines.
CameraStateproperties are exposed asMutableStateFlow. Platform engines collect these flows on their respective dispatchers. Crucially, no callbacks cross the public API boundary; you should interact with the state via Flows. - Compose Lifecycle Management: Camera startup and teardown are managed by the Compose lifecycle rather than imperative commands. The
CameraPreviewcomponent usesDisposableEffectto automatically bind and unbind the camera engine to the composable's lifecycle. There is no manualstart()orstop()API to call.
- KMP expect/actual: The core camera components (
Supported Platforms for Camposer
mainCamposer is a Kotlin Multiplatform library that currently supports the following platforms:
- Android: Fully supported
- iOS: Fully supported
How state changes are applied to hardware
mainCamposer uses a reactive data flow to ensure hardware state stays in sync with the UI. When a user changes a setting (like flash mode), the following sequence occurs:
- User Action: The user calls a method on the
CameraControlleror mutates a property inCameraState(e.g.,controller.setFlashMode(FlashMode.On)). - Engine Update: The
CameraEnginereceives the command. - Applier Delegation: The
CameraEngineImpldelegates the specific concern to a specialized Applier (e.g.,ExposureZoomApplier). - Platform Execution: The Applier calls the platform-specific API (Android's
CameraXor iOS'sAVFoundation). - State Confirmation: Finally, the
cameraStateis updated to reflect the actual hardware state. This ensures the state write is always the last step in the cycle.
- User Action: The user calls a method on the
Configure camera capture mode
mainThe
captureModeparameter determines whether the camera is in photo capture or video recording mode. This mode is passed to theCameraPreviewcomponent.Available modes:
CaptureMode.Image(default): Enables still image capture functionality.CaptureMode.Video: Enables video recording functionality and makes recording-related APIs available.
You can manage the mode using a state variable (e.g., using Compose's
mutableStateOf) to toggle between modes dynamically.var captureMode by remember { mutableStateOf(CaptureMode.Image) } val cameraSession = rememberCameraSession() CameraPreview( cameraSession = cameraSession, captureMode = captureMode, ) { Button( onClick = { captureMode = when (captureMode) { CaptureMode.Image -> CaptureMode.Video CaptureMode.Video -> CaptureMode.Image } } ) { Text( "Switch to ${if (captureMode == CaptureMode.Image) "Video" else "Image"} Mode" ) } }Choose an Orientation Strategy
mainCamposer provides two orientation strategies:
OrientationStrategy.Preview: Matches the camera preview's UI orientation. This provides a "what you see is what you get" result, regardless of how the device is rotated. Note: This mode is currently only supported on iOS.OrientationStrategy.Device: Matches the device's physical orientation. The output reflects how the device is physically held, even if the UI is locked.
Understand the Code Scan result object
mainWhen a code is successfully scanned, the result object provides the following data:
text: The decoded text content.type: The detectedCodeType.frame: A rectangle representing the detected area (useful for 2D drawing).cornerPoints: The specific corner points of the code (better suited for 3D rendering).
How CameraController works with CameraPreview
mainThe
CameraControllerdecouples camera operations (like taking pictures or recording videos) from the UI layer. Instead of performing actions directly on a session object within a Composable, you use aCameraControllerto manage operations.To use it, you instantiate a
CameraController, create acameraSessionusingrememberCameraSession(controller), and pass that session to theCameraPreviewcomposable. This allows you to trigger camera actions from anywhere (like a ViewModel) while theCameraPreviewhandles the actual rendering.val controller = remember { CameraController() } val cameraSession = rememberCameraSession(controller) CameraPreview( cameraSession = cameraSession, )How Camera Format configuration priority works
mainWhen using a custom
CamFormat, Camposer uses a scoring and fallback mechanism to find the best match on the device. It attempts to satisfy the provided configurations in descending order of complexity.If the full set of requested configurations cannot be met, it falls back to the following priority sequence:
ResolutionConfig+AspectRatio+FrameRateConfig+VideoStabilizationConfigResolutionConfig+AspectRatio+FrameRateConfigResolutionConfig+AspectRatio+VideoStabilizationConfigResolutionConfig+AspectRatioResolutionConfig+FrameRateConfigResolutionConfig+VideoStabilizationConfigResolutionConfig