This document provides a technical breakdown of the architecture, layer boundaries, module blueprint, key workflows, storage design, and format compatibility for LosslessCut (Android).
LosslessCut follows MVVM + Clean Architecture with strict layer boundaries, reactive state management via Kotlin Coroutines/Flows, and native Android media processing.
┌───────────────────────────────────────────┐
│ :app │
│ (UI, Fragments, Jetpack ViewModels) │
└──────┬─────────────────────────────┬──────┘
│ │
runtimeOnly│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ :engine │ │ :core:data │
│(Media Engine)│ │(Repositories)│
└──────┬───────┘ └──────┬───────┘
│ │
└──────────────┬─────────────┘
│
▼
┌───────────────────────────────────────────┐
│ :core:domain │
│ (Pure JVM Library: Use Cases & Models) │
└───────────────────────────────────────────┘
- Languages: Kotlin 2.2+, Gradle Kotlin DSL (
.gradle.kts) - Media Processing: Native
MediaExtractor,MediaMuxer(in:engine), Media3 / ExoPlayer (Playback UI in:app) - Dependency Inversion & Injection: Domain-level interfaces (
ILosslessEngine,IVideoEditingRepository,IMediaFinalizer) resolved via Hilt (:app,:engine,:core:data) - SDK Targets: Min SDK 26 (Android 8.0), Target SDK 36 (Android 15 / "Baklava")
- Build System: AGP 9.0+, JDK 17/21 Toolchain
- Native Threading: All
MediaExtractor,MediaCodec, andMediaMuxerwork is serialized through a dedicated single-threaded engine dispatcher to avoid unsafe concurrent native calls.
To prevent technical debt and maintain zero-loss performance, the following rules are enforced across the codebase:
Important
- Module Isolation:
:appincludes:enginestrictly viaruntimeOnly(:engine). Direct code imports of engine classes inside:appare forbidden; all invocation flows through Hilt and:core:domaininterfaces (ILosslessEngine). - Pure JVM Domain:
:core:domainmust remain a pure JVM Kotlin library. Zeroandroid.*,androidx.*, or Hilt dependencies allowed. - Storage Access Policy: Shared user media must be accessed exclusively through SAF (
DocumentFile) orContentResolver/MediaStore. Directjava.io.Fileaccess on external storage is strictly forbidden. - UI Framework Scoping: Jetpack Compose is restricted to
:app/ui/compose/**for modular dialogs/sheets. NLE timeline scrubbing and video player UI rely on custom AndroidViewcomponents.
:app: Android UI & presentation.ui/:MainActivitydashboard,EditorFragment, recent-session UI,PlayerManager, andShortcutHandler.customviews/: Timeline scrubbing (CustomVideoSeeker,TimelineViewport,SeekerRenderer,SeekerGhostRenderer,SeekerAccessibilityHelper).ui/compose/: Isolated Compose dialogs and sheets.viewmodel/:VideoEditingViewModelorchestrating UI events and delegating state toEditingSession.
:core:domain: Core business domain (Pure JVM).session/:EditingSessiondomain aggregate managing segment boundaries, undo/redo stacks, and dirty state.model/:MediaClip,TrimSegment,FrameAnalysis,VisualDetectionConfig,WaveformResult.usecase/:ExportUseCase,SilenceDetectionUseCase,SegmentDetectorUseCase,ClipManagementUseCase,SessionUseCase,ExtractSnapshotUseCase.engine/&repository/: Domain interfaces (ILosslessEngine,IVideoEditingRepository,IMediaFinalizer,IVisualSegmentDetector).
:engine: Native media extraction & muxing engine.muxing/: Deep pipeline (MuxingPipeline,ExtractorSampleCopier,MuxerWriter,MergeValidator,SegmentGapCalculator,TrackInspector,SampleTimeMapper).VisualAlgorithms.kt&VisualSegmentDetectorImpl.kt: Frame pHash, SAD, and Laplacian variance analysis.AudioWaveformExtractor.kt&AudioDecoderImpl.kt: Low-level PCM audio decoding and amplitude extraction.
:core:data: Storage, repositories, and preferences.data/:VideoEditingRepositoryImpl,AnalysisCacheImpl,AppPreferences(DataStore).AnalysisCacheImpl: App-private, versioned binary cache for waveform and visual frame-analysis results with LRU size eviction and age-based expiry.utils/:StorageUtilsfor SAF tree creation and MediaStore operations.di/:MediaFinalizerImplimplementation ofIMediaFinalizer.
- Performance: Uses
LruCachewaveform bitmap tile caching (2048px tiles) to eliminate per-frame canvas line rendering during scrubbing. - Interactivity: Multi-touch zoom (up to 20x), top-anchored segment edge drag handles (preventing system gesture navigation conflicts), playhead/segment edge drag gestures, auto-panning, split line preview, and anchored delete/split actions on long press. Long press resolves KEEP segments before the playhead target when the playhead overlaps a segment.
- Accessibility: Virtual view hierarchy via
ExploreByTouchHelper(SeekerAccessibilityHelper).
-
Silence Detection: Analyzes raw PCM amplitudes (
AudioWaveformExtractor) to compute RMS energy levels without noise floor distortion. -
Visual Detection: Refactored to operate on exact frame-step logic (
sampleIntervalFrames, e.g. every 1st, 2nd, 5th, 10th frame) instead of millisecond intervals. UsesMediaExtractorframe stepping (decodedFrameIndex % sampleIntervalFrames == 0) with Kotlin-native perceptual hashing (pHash), normalized Sum of Absolute Differences rate (Luminance Delta/sec), and contrast-normalized Laplacian variance ($\frac{\text{Laplacian Variance}}{(\text{Mean Luma}/255.0)^2 + 0.05}$ ) with area-average box downsampling to eliminate spatial aliasing noise. Freeze frame ranges feature automatic start-timestamp backdating to frame$t_{i-1}$ to eliminate sampling lag. Includes post-input-EOS timeout protection (MAX_EOS_TIMEOUTS) to prevent hardware decoder drain hangs. -
Segment Application:
SilenceDetectionUseCase.applyDetectionRanges()intersects newly detected ranges with existing clipTrimSegmentbounds to preserve prior user clip trims. -
Coroutine Cancellation Contract: All low-level
MediaCodecandMediaExtractorloops enforce cooperative cancellation viacoroutineContext.ensureActive()and rethrowCancellationExceptionto guarantee immediate resource release upon cancellation.
VideoEditingViewModel: Unidirectional data flow state machine emitting single-shot UI events viaChannel<VideoEditingEvent>with undo/redo segment history stacks.- Keyboard Shortcuts (
ShortcutHandler): Desktop/hardware keyboard controls (SPACEtoggle play,I/Osegment bounds with keyframe snapping and smart segment creation/adjustment,Ssplit,LEFT/RIGHTkeyframe seek).
MainActivityexposes one Load media action. File opens, share intents, and recent-session cards all navigate toVideoEditingActivitywith the editor as the only navigation destination.- Dirty editing state is serialized to app-private cache storage when the editor stops. The dashboard maintains the five most recent sessions, validates source URI access before resume, and removes unavailable, discarded, or successfully exported sessions.
- Export choices are grouped in one full-screen
ExportOptionsDialogPresentersurface: combined or separate output, inline rotation metadata, track selection, and optional post-export move to System Trash / SAF document deletion.
flowchart TD
Domain["EditingSession Aggregate (:core:domain)"] -->|Emits EditingSessionState| StateFlow["StateFlow in VideoEditingViewModel"]
StateFlow -->|Exposes UI State| Fragment["EditorFragment UI Container"]
Fragment -->|Updates Geometry & Handles| Seeker["CustomVideoSeeker Timeline View"]
Fragment -->|Controls Playback| Player["PlayerManager (ExoPlayer)"]
LosslessCut writes output media directly to destination URIs without requiring full temp file copies on shared storage:
[Muxer Pipeline] ──> ParcelFileDescriptor / Temp Working File
│
▼
[StorageUtils.createMediaOutputUri]
│
┌───────────────┴───────────────┐
▼ ▼
[Custom SAF Tree URI] [MediaStore Collection]
(DocumentFile.createFile) (IS_PENDING = 1 API 29+)
│ │
└───────────────┬───────────────┘
▼
[IMediaFinalizer]
(Set IS_PENDING = 0)
- SAF Fallback: If a custom directory is selected in preferences,
StorageUtilscreates the output file viaDocumentFile.fromTreeUri(). - MediaStore Lifecycle: On Android 10+ (API 29+), output files in public MediaStore collections are marked
IS_PENDING = 1during writing and updated to0upon completion. - Non-MediaStore URIs:
MediaFinalizerImplcatchesUnsupportedOperationExceptionwhen invokingIS_PENDING = 0on SAF or FileProvider URIs. - Finalization Order: The muxer is stopped and released, and the output descriptor is closed, before
IMediaFinalizerpublishes the URI. Failed exports remove their incomplete output where the provider permits it.
Derived analysis data is separate from user media and is stored under the app's private noBackupFilesDir/analysis_cache directory. IAnalysisCache in :core:domain defines the platform-neutral contract; AnalysisCacheImpl in :core:data provides the Android file-backed implementation.
- Waveforms and visual frame analyses use versioned binary payloads and atomic replacement on write.
- Cache keys include the source URI, media metadata, and analysis parameters so a changed clip or configuration does not reuse stale results.
- Reads refresh access time. Expired entries are removed by age, then least-recently-used entries are removed until the configured byte cap is met.
- Settings persist the maximum size (50–1000 MiB) and retention age (1–90 days). Users can inspect current usage or clear all derived analysis data; clearing does not affect source media or editing sessions.
Editing sessions are separate from source media and analysis data. VideoEditingRepositoryImpl stores serialized clip state and a small recency index under the app-private noBackupFilesDir/editing_sessions; source files remain user-owned SAF/ContentResolver URIs. The index is capped at five sessions and is treated as recoverable convenience state rather than a backup. A session is removed when the user discards it or an export completes, and stale entries are removed when their source URI is no longer readable. During development, the old cache-based location is intentionally not migrated.
sequenceDiagram
autonumber
participant UI as EditorFragment / ViewModel
participant UC as ExportUseCase
participant Engine as LosslessEngineImpl
participant Copier as ExtractorSampleCopier
participant Muxer as MuxerWriter
participant Fin as IMediaFinalizer
UI->>UC: exportSegments(uri, segments)
UC->>Engine: executeLosslessCut(uri, startMs, endMs)
Engine->>Copier: Seek to sync keyframe <= startMs
Copier->>Muxer: Copy encoded samples (PTS >= startMs & <= endMs)
Engine->>Fin: finalizeVideo(outputUri)
Fin-->>UI: Export Complete (MediaStore URI)
sequenceDiagram
autonumber
participant Engine as LosslessEngineImpl
participant Val as MergeValidator
participant Calc as SegmentGapCalculator
participant Muxer as MuxerWriter
Engine->>Val: validateCompatibility(inputClips)
Val-->>Engine: Stream & Codec Parameters Match
Engine->>Calc: computeCumulativeTimeOffsets(inputSegments)
loop Each Media Segment
Engine->>Muxer: Write samples with shifted PTS
end
Engine->>Muxer: Close MediaMuxer & output descriptor
Muxer->>Fin: Publish completed output URI
- Module Test Command:
./scripts/dev-scripts/gradle-test.sh <module> <pattern> - Project Verification:
./scripts/dev-scripts/project-verify.sh(Runs Detekt, JVM unit tests, Lint, and Kover sequentially to avoid generated-source races.) - Konsist Architectural Tests:
ArchitectureTest.ktautomatically enforces all 4 architectural guardrails in CI (preventing:engineimports in:app, Android/Hilt imports in:core:domain,java.io.Filefor shared storage, and Compose in custom views). - Kover Code Coverage Targets: Core domain aggregate
EditingSession(98.3%) and media processing engineMuxingPipeline(93.8%) maintain coverage well above the repository >80% threshold. - Engine Instrumented Tests: Engine tests relying on native Android codecs MUST reside in
:engine/src/androidTest(not:app). - FileProvider Mocking: Instrumented tests use local storage / mocked
FileProviderauthorities (:engine:connectedDebugAndroidTest). - TargetSdk 33+ Test Asset Staging: TargetSdk 33+ instrumented tests requiring media assets copy files to
cacheDirviaUiAutomation.executeShellCommand().
LosslessCut operates strictly at the container & bitstream level using Android native MediaMuxer:
| Category | Lossless Direct Support | Container Remuxing / Notes |
|---|---|---|
| Output Containers | .mp4, .m4a (audio-only) |
Formats requiring transcoding (MP3, FLAC, VP9) are excluded to ensure zero loss. |
| Video Codecs | H.264 (AVC), H.265 (HEVC) | H.263, MPEG-4 Visual supported where container allows. |
| Audio Codecs | AAC (LC, HE) | AMR-NB, AMR-WB supported natively. |
| Input Containers | .mp4, .m4a, .mov, .mkv* |
*Remuxable to MP4 without re-encoding if internal audio/video codecs match target limits. |
Rotation metadata is configured inline on the unified lossless export surface for the current MP4/M4A output path. Generic title, artist, and creation-date tag writing is not implemented yet; see the Advanced Tags roadmap item in README.md.
In accordance with Agentic Engineering standards, all code modifications are classified into three operational risk tiers with mandatory verification gates:
| Risk Tier | Scope & Features | Required Verification Gates |
|---|---|---|
| Low | UI layouts, Compose sheets, strings, formatting, docs | Focused JVM unit tests, Detekt linting (./scripts/dev-scripts/project-verify.sh). |
| Medium | Smart Cut algorithms (Silence/Visual), EditingSession mutations, ViewModel logic |
Module JVM tests (SilenceCutUseCaseTest, SegmentDetectorTest), cache retention tests. |
| High / Critical | Native Muxing pipeline (:engine), SAF storage (StorageUtils), remuxing, MediaStore finalization |
Full ./scripts/dev-scripts/project-verify.sh, SAF file write verification, single-thread MediaMuxer assertion. |
For external AI documentation lookups:
/androidx/media(Media3 / ExoPlayer)/kotlin/kotlinx.coroutines(Coroutines & Flows)/androidx/datastore(Preferences DataStore)/material-components/material-components-android(Material 3 UI)