Skip to content

Debanshu777/Popp

Repository files navigation

Popp

Modern Android camera. Jetpack Compose + CameraX. Live Dual-Camera composite · Live Draggable Primary/PiP Swap Mid-Recording (GLES + MediaCodec) · app-private gallery.


Kotlin AGP minSdk compileSdk CameraX Compose Hilt


Popp screen tour


Features

📸 Photo ZERO_SHUTTER_LAG 🎥 Video HIGHEST / HD dual 🔀 Live dual-camera one MP4 ♻️ Mid-record PiP swap no split
🎨 5 composite layouts 🖐️ Draggable PiP off recompose path 🖼️ Private gallery prefix-filtered 🔐 Queue-based perms auto-advance

Demo

Popp in action

Camera Modes

Single camera
Single camera — flash, zoom presets, lens flip via cameraControl
Draggable PiP
Draggable PiP — pinch 20–55% canvas width, snap-to-corner on release

Composite Layouts

Tap the preview to cycle DualLayout. Each variant is a branch in the GL shader.


PIP_RECT

PIP_CIRCLE

SPLIT_VERTICAL

SPLIT_HORIZONTAL

REACTION_STRIP

Layout cycling blocked mid-recording — encoder canvas is dim-locked at record-start.

Gallery

Gallery

LazyColumn + stickyHeader per group. Toggle DailyMonthly grouping without re-querying.

DISPLAY_NAME LIKE 'POPP_%' selection means the gallery shows only Popp captures — files written without the POPP_IMG_ / POPP_VID_ prefix are invisible by design.


Architecture

Single-Activity Compose app. MainActivityPoppTheme → permission gate → MainContent (Navigation3).

com.debanshu777.popp
├── data/{camera,media}    — repositories + MediaStoreGateway
├── di/                    — Hilt modules + dispatcher qualifiers
├── navigation/            — Navigation3 destinations
└── ui/features/{camera,gallery,permissions}

Per-action use cases. ViewModels orchestrate UI only. Hardware state lives in CameraSessionRepository (StateFlow<CameraSessionState>). @ApplicationContext injected in exactly two places: DefaultCameraProviderRepository, MediaStoreGateway.

📷 Live dual-camera swap — GLES + MediaCodec composite pipeline

ConcurrentCamera + CompositionSettings is bind-time only — any layered solution that re-binds to swap PiP kills the encoder and splits the MP4. Popp composites itself.

back cam ──→ SurfaceTexture(back) ─┐
                                    ├─→ GL composite shader ─→ shared EGL ┬─→ SurfaceView (preview)
front cam ─→ SurfaceTexture(front) ┘    (reads dualBackPrimary)           └─→ MediaCodec ─→ MediaMuxer ─→ MP4
  • Two Preview use cases, one SurfaceTexture each, bound via ConcurrentCamera.bindToLifecycle(listOf(...)).
  • HandlerThread-backed renderer owns EGL14 context with two eglSurfaces (preview + encoder) sharing the same context.
  • Primary/PiP = one @Volatile var backPrimary: Boolean. Flip → next frame swaps texture roles. No rebind.
  • Encoder PTS via eglPresentationTimeANDROID(...) from the back camera's SurfaceTexture.getTimestamp(), rebased to start at 0.
  • Encoder dims computed at record-start from viewport pixel aspect, capped 1280 long edge, rounded DOWN to 16-multiple. orientationHint = 0.

Rotation gotcha — CameraX's SurfaceTexture.getTransformMatrix(...) already bakes display rotation. Stacking Matrix.rotateM(...) on top → 90° tilted preview. Tex matrix order: ST · mirror · crop. No rotation.

Deep dive → docs/06-live-dual-swap.md

🏗️ Use-case architecture — per-action use cases, repositories, sealed Result types
  • Use cases own threading. Blocking work wrapped in withContext(@IoDispatcher). CameraX binds on Dispatchers.Main.immediate.
  • CameraSessionRepository is a singleton StateFlow<CameraSessionState> holding live Camera, ImageCapture, VideoCapture, SurfaceRequest, Recording, DualCameraSession, DualVideoEncoder.
  • Start/Stop/ToggleVideoRecording/ToggleDualRecording use cases return sealed Result unions. VM when-switches once, pushes snackbar + UiState.
  • Use cases never inject raw Context. Writes go through MediaStoreGateway, reads through MediaRepository.

Deep dive → docs/07-usecase-architecture.md

🖼️ Camera preview + capture — SurfaceRequest, MediaStore, prefix contract

CameraXViewfinder (from androidx.camera.compose) consumes a SurfaceRequest directly — no AndroidView, no SurfaceHolder callbacks.

LifecycleStartEffect(useFront, useDualCamera, dualBackPrimary) {
    viewModel.startCamera(lifecycleOwner)
    onStopOrDispose { viewModel.stopCamera() }
}

Photo↔Video is not a rebind key — both use cases are pre-bound in single mode.

All writes go through MediaStoreDCIM/Popp/ → prefixed filenames. Same prefix is the read-side LIKE selection. App-private gallery, scoped-storage compliant.

Deep dives → 01-camera-preview, 02-photo-and-video-capture

🔐 Permissions — sealed enum + queue, auto-advancing UI

Three layers:

  1. Activity gate — main UI composes only when MultiplePermissionsState.allPermissionsGranted.
  2. Sealed PermissionEnum — each branch owns manifest string, icon, strings, optionality flag.
  3. One-screen queuePermissionsViewModel walks queue in order. Optional perms (RECORD_AUDIO) drop on first decline; required perms stay until granted or settings deep-link.

SDK split (TIRAMISU+) for READ_MEDIA_IMAGES / READ_MEDIA_VIDEO vs READ_EXTERNAL_STORAGE lives in two places — MainActivity.requiredPermissions and PermissionEnum.getRequiredPermissions() — keep them in sync.

Deep dive → docs/05-permissions.md


Build

./gradlew assembleDebug              # Build debug APK
./gradlew installDebug               # Install on device/emulator
./gradlew lint                       # Android lint
./gradlew test                       # JVM unit tests
./gradlew connectedDebugAndroidTest  # Instrumented tests

Device requirements

Feature Requirement
Single-camera + gallery API 24+
Dual-camera recording API 26+ (MediaMuxer(FileDescriptor), IS_PENDING are 26+/29+)
Dual-camera button PackageManager.FEATURE_CAMERA_CONCURRENT + back+front pair in availableConcurrentCameraInfos

Docs

# Doc Topic
01 Camera preview pipeline SurfaceRequestCameraXViewfinder, lifecycle keys
02 Photo & video capture MediaStore writes, prefix contract
03 Concurrent (dual) camera Legacy ConcurrentCamera (historical)
04 Recording the dual feed Legacy CompositionSettings recording (historical)
05 Permissions Accompanist + sealed-enum queue
06 Live dual-camera swap GLES + MediaCodec composite pipeline
07 Use-case architecture Per-action use cases, repositories, threading

Built by @debanshu777

About

Modern Android Concurrent Camera. Jetpack Compose + CameraX. Live dual-camera composite · PiP Swap Mid-Recording (GLES + MediaCodec) · Scoped-Storage

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages