Camposer

repository·main·Indexed 43 days ago

https://github.com/ujizin/camposer

A 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.

Tokens
14.5K
Snippets
50
Records
67
Agent score
55%

What's inside Camposer

  1. Overview of Camposer

    main
    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.
  2. Camposer Android Stack Overview

    main

    The 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.
  3. Capture and Recording workflows

    main

    Camposer handles still captures and video recording through the CameraController using 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) or CaptureResult.Failure(e) to the provided callback on the caller's executor/dispatcher.

    Video Recording

    • Starting: CameraController.startRecording(...) triggers the platform's RecordController. On Android, this uses VideoCapture.output.prepareRecording(); on iOS, it uses an AVAssetWriter pipeline. The CameraSession updates its isRecording flag to true.
    • Stopping: CameraController.stopRecording() finalizes the recording, delivers the CaptureResult, and sets isRecording to false in the CameraSession.
  4. How Camposer's public API works

    main

    Camposer 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 @Composable function. It renders the camera feed and manages the UI lifecycle.
    • CameraSession: The primary state holder, created using rememberCameraSession(). It is an expect class that provides access to the session's state, hardware info, and controller.
    • CameraState: A collection of configurable camera properties exposed as MutableStateFlow (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, including takePicture, startRecording, and stopRecording.
    • 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")
            }
        }
    }
  5. Understand cross-cutting concerns in Camposer

    main

    Camposer 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's expect/actual mechanism. If you need to add a method to these components, you must update the common expect declaration and provide the corresponding actual implementations for all supported platforms.
    • Coroutines for Async State: All asynchronous state is driven by Coroutines. CameraState properties are exposed as MutableStateFlow. 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 CameraPreview component uses DisposableEffect to automatically bind and unbind the camera engine to the composable's lifecycle. There is no manual start() or stop() API to call.
  6. How state changes are applied to hardware

    main

    Camposer 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:

    1. User Action: The user calls a method on the CameraController or mutates a property in CameraState (e.g., controller.setFlashMode(FlashMode.On)).
    2. Engine Update: The CameraEngine receives the command.
    3. Applier Delegation: The CameraEngineImpl delegates the specific concern to a specialized Applier (e.g., ExposureZoomApplier).
    4. Platform Execution: The Applier calls the platform-specific API (Android's CameraX or iOS's AVFoundation).
    5. State Confirmation: Finally, the cameraState is updated to reflect the actual hardware state. This ensures the state write is always the last step in the cycle.
  7. Configure camera capture mode

    main

    The captureMode parameter determines whether the camera is in photo capture or video recording mode. This mode is passed to the CameraPreview component.

    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"
            )
        }
    }
  8. Choose an Orientation Strategy

    main

    Camposer 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.
  9. Understand the Code Scan result object

    main

    When a code is successfully scanned, the result object provides the following data:

    • text: The decoded text content.
    • type: The detected CodeType.
    • frame: A rectangle representing the detected area (useful for 2D drawing).
    • cornerPoints: The specific corner points of the code (better suited for 3D rendering).
  10. How CameraController works with CameraPreview

    main

    The CameraController decouples 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 a CameraController to manage operations.

    To use it, you instantiate a CameraController, create a cameraSession using rememberCameraSession(controller), and pass that session to the CameraPreview composable. This allows you to trigger camera actions from anywhere (like a ViewModel) while the CameraPreview handles the actual rendering.

    val controller = remember { CameraController() }
    val cameraSession = rememberCameraSession(controller)
    
    CameraPreview(
        cameraSession = cameraSession,
    )
  11. How Camera Format configuration priority works

    main

    When 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:

    1. ResolutionConfig + AspectRatio + FrameRateConfig + VideoStabilizationConfig
    2. ResolutionConfig + AspectRatio + FrameRateConfig
    3. ResolutionConfig + AspectRatio + VideoStabilizationConfig
    4. ResolutionConfig + AspectRatio
    5. ResolutionConfig + FrameRateConfig
    6. ResolutionConfig + VideoStabilizationConfig
    7. ResolutionConfig