Skip to content

Repository files navigation

GitHub Downloads (all assets, all releases)

LightCompressor Enhanced

A powerful and easy-to-use Android video compression library using MediaCodec. Generates compressed MP4 with configurable resolution, bitrate, and codec while maintaining good visual quality.

Installation

Add the JitPack repository to your settings.gradle:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

Add the dependency in your module-level build.gradle:

implementation 'com.github.davotoula:LightCompressor-enhanced:Tag'

You also need Kotlin coroutines:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

Quick Start

import com.davotoula.lightcompressor.VideoCompressor
import com.davotoula.lightcompressor.VideoQuality
import com.davotoula.lightcompressor.config.Configuration
import com.davotoula.lightcompressor.config.SharedStorageConfiguration
import com.davotoula.lightcompressor.config.SaveLocation
import com.davotoula.lightcompressor.listener.CompressionListener

VideoCompressor.start(
    context = applicationContext,
    uris = listOf(videoUri),
    isStreamable = true,
    storageConfiguration = SharedStorageConfiguration(
        saveAt = SaveLocation.movies,
        subFolderName = "my-videos"
    ),
    configureWith = Configuration(
        videoNames = listOf("output.mp4"),
        quality = VideoQuality.MEDIUM,
        isMinBitrateCheckEnabled = true,
        disableAudio = false,
    ),
    listener = object : CompressionListener {
        override fun onStart(index: Int) {
            // Compression started
        }

        override fun onProgress(index: Int, percent: Float) {
            // Update progress UI (worker thread — post to main thread if needed)
        }

        override fun onSuccess(index: Int, size: Long, path: String?) {
            // Compression finished; path is the output file location
        }

        override fun onFailure(index: Int, failureMessage: String) {
            // Handle error
        }

        override fun onCancelled(index: Int) {
            // Compression was cancelled
        }
    }
)

// To cancel a running compression:
VideoCompressor.cancel()

Features

  • H.264 (AVC) and H.265 (HEVC) codec support
  • Flexible VideoResizer API for resolution control:
    • VideoResizer.auto — auto-resize based on original dimensions
    • VideoResizer.scale(0.5) — scale by percentage
    • VideoResizer.limitSize(1920.0) — limit longest side (landscape-oriented bounding box)
    • VideoResizer.limitSize(1920.0, 1080.0) — limit width and height independently
    • VideoResizer.limitShortSide(1080.0) — constrain the shorter dimension, orientation-agnostic (preferred for portrait/landscape-neutral targeting)
    • VideoResizer.limitShortSide(1920.0, 1080.0) — same as above, uses the smaller of the two values
    • VideoResizer.matchSize(1920.0, 1080.0) — scale to match target dimensions exactly
  • GIF to MP4 conversion
  • Streamable output (moov atom moved for progressive download)
  • Granular bitrate control: specify in Mbps (videoBitrateInMbps) or bps (videoBitrateInBps)
  • Audio control (disableAudio)
  • Native Android MediaMuxer (no third-party MP4 muxer)

Configuration

VideoQuality

Quality Bitrate multiplier
VERY_HIGH 0.6x original
HIGH 0.4x original
MEDIUM 0.3x original
LOW 0.2x original
VERY_LOW 0.1x original

Bitrate Options

  • videoBitrateInMbps: Int? — custom bitrate in Mbps
  • videoBitrateInBps: Long? — custom bitrate in bps (takes precedence over videoBitrateInMbps)
  • isMinBitrateCheckEnabled: Boolean — when true, skips compression if source bitrate is below 2 Mbps

Codec Selection

import com.davotoula.lightcompressor.VideoCodec
import com.davotoula.lightcompressor.utils.CompressorUtils

val codec = if (CompressorUtils.isHevcEncodingSupported()) {
    VideoCodec.H265  // Better compression, smaller files
} else {
    VideoCodec.H264  // Maximum device compatibility (default)
}

val config = Configuration(
    videoNames = listOf("output.mp4"),
    quality = VideoQuality.MEDIUM,
    videoCodec = codec,
)

If VideoCodec.H265 is requested on a device that does not support HEVC encoding, onFailure is called with a descriptive error message.

Storage Options

SharedStorageConfiguration

Saves to shared storage (Movies, Pictures, or Downloads).

import com.davotoula.lightcompressor.config.SharedStorageConfiguration
import com.davotoula.lightcompressor.config.SaveLocation

SharedStorageConfiguration(
    saveAt = SaveLocation.movies,  // or .pictures / .downloads
    subFolderName = "my-videos"    // optional
)

AppSpecificStorageConfiguration

Saves to the app's private external storage directory.

import com.davotoula.lightcompressor.config.AppSpecificStorageConfiguration

AppSpecificStorageConfiguration(
    subFolderName = "compressed"  // optional subfolder
)

CacheStorageConfiguration

Saves to the app's cache directory (may be cleared by the system).

import com.davotoula.lightcompressor.config.CacheStorageConfiguration

CacheStorageConfiguration()

Custom Storage

Implement StorageConfiguration for full control over where files are written.

import com.davotoula.lightcompressor.config.StorageConfiguration

class MyStorageConfiguration : StorageConfiguration {
    override fun createFileToSave(
        context: Context,
        videoFile: File,
        fileName: String,
        shouldSave: Boolean
    ): File {
        // Return the File where the output should be written
    }
}

Permissions

API < 29

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28"
    tools:ignore="ScopedStorage" />

API 29 – 32

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32"/>

API >= 33 (Photo Picker recommended)

<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

HLS Preparation

HlsPreparer transcodes a local video into multiple HLS VOD renditions (fMP4 segments + m3u8 playlists) for adaptive-bitrate playback.

API

  • HlsPreparer.start(context, uri, config, listener): Job — kicks off preparation and returns a coroutine Job.
  • HlsPreparer.cancel() — cancels any running preparation.
  • HlsConfig(ladder, codec, segmentDurationSeconds, disableAudio, singleFilePerRendition) — configuration. Defaults: HlsLadder.default(), VideoCodec.H264, 6 s segments, audio enabled, multi-file output. Set singleFilePerRendition = true to emit one combined fMP4 file per rendition (init + every media segment) referenced by #EXT-X-BYTERANGE in the playlist — useful when the consumer wants a single upload per rendition instead of dozens of segment files.
  • HlsLadder — ordered list of Renditions. Use HlsLadder.default() (360p / 540p / 720p / 1080p / 4K) and chain .drop("1080p", "4K") or .add(Rendition(Resolution.HD_720, 2500)) to customise. Renditions whose short side exceeds the source are automatically dropped at start.
  • Rendition(resolution: Resolution, bitrateKbps: Int) — a single ladder entry. Resolution is the same enum used by VideoCompressor (SD_360, SD_540, HD_720, FHD_1080, UHD_4K).
  • HlsListener — 8 callbacks: onStart(renditionCount), onRenditionStart(rendition), onSegmentReady(rendition, segment), onRenditionComplete(rendition, summary), onComplete(masterPlaylist), onFailure(error), onProgress(rendition, percent), onCancelled(). HlsRenditionSummary carries the media playlist plus output dimensions, codec string, and the library's chosen filenames. Subclass SimpleHlsListener if you only care about a subset of events.
  • HlsSegment(file, index, durationSeconds, isInitSegment, isCombinedRendition) — one emitted segment. file is a temp file that is valid only until onSegmentReady returns — copy or upload it synchronously. In multi-file mode isInitSegment = true for the per-rendition init.mp4. In single-file mode the listener receives exactly one callback per rendition with isCombinedRendition = true; the file contains the init segment followed by every media fragment.
  • HlsError(message, failedRenditions, completedRenditions) — delivered to onFailure when every rendition fails. Partial failures still trigger onComplete.

Threading: onSegmentReady and onProgress are invoked on a background dispatcher (Dispatchers.Default); all other callbacks are on the main thread.

Output layout (multi-file, default): segments are identified by the rendition's Resolution.label (e.g. 720p). Per rendition, HlsPreparer emits one init.mp4 followed by segment_000.m4s, segment_001.m4s, … The per-rendition media playlist (media.m3u8) is delivered as a String via onRenditionComplete, and the master playlist (master.m3u8) is delivered as a String via onComplete. Persisting playlists and segments to their final destination (disk, CDN, object storage) is the caller's responsibility — a typical layout is master.m3u8 at the root with one subdirectory per rendition label containing media.m3u8, init.mp4, and the segment_NNN.m4s files.

Output layout (single-file): with singleFilePerRendition = true, each rendition produces one <label>.mp4 file (e.g. 720p.mp4) containing the init segment immediately followed by every media fragment. The media playlist references the file via #EXT-X-MAP:URI="<label>.mp4",BYTERANGE="<initLen>@0" and uses #EXT-X-BYTERANGE for each #EXTINF entry, so a typical persisted layout is master.m3u8 at the root with one subdirectory per rendition label containing media.m3u8 and <label>.mp4.

Usage

import com.davotoula.lightcompressor.HlsPreparer
import com.davotoula.lightcompressor.VideoCodec
import com.davotoula.lightcompressor.hls.HlsConfig
import com.davotoula.lightcompressor.hls.HlsError
import com.davotoula.lightcompressor.hls.HlsLadder
import com.davotoula.lightcompressor.hls.HlsListener
import com.davotoula.lightcompressor.hls.HlsRenditionSummary
import com.davotoula.lightcompressor.hls.HlsSegment
import com.davotoula.lightcompressor.hls.Rendition
import com.davotoula.lightcompressor.hls.suggestedFilename
import java.io.File

val outputRoot = File(context.filesDir, "hls-out").apply { mkdirs() }

val config = HlsConfig(
    ladder = HlsLadder.default().drop("4K"),
    codec = VideoCodec.H264,
    segmentDurationSeconds = 6,
    disableAudio = false,
)

HlsPreparer.start(
    context = applicationContext,
    uri = videoUri,
    config = config,
    listener = object : HlsListener {
        override fun onStart(renditionCount: Int) { /* prep UI */ }

        override fun onRenditionStart(rendition: Rendition) { /* optional */ }

        override fun onSegmentReady(rendition: Rendition, segment: HlsSegment) {
            // Called on a background thread. Copy synchronously — the temp
            // file is deleted as soon as this method returns.
            val dest = File(outputRoot, rendition.suggestedFilename(segment))
            dest.parentFile?.mkdirs()
            segment.file.copyTo(dest, overwrite = true)
        }

        override fun onRenditionComplete(rendition: Rendition, summary: HlsRenditionSummary) {
            val mediaPlaylistFile = File(outputRoot, summary.playlistFilename)
            mediaPlaylistFile.parentFile?.mkdirs()
            mediaPlaylistFile.writeText(summary.mediaPlaylist)
        }

        override fun onComplete(masterPlaylist: String) {
            val master = File(outputRoot, "master.m3u8").apply { writeText(masterPlaylist) }
            // Hand off to ExoPlayer (requires androidx.media3:media3-exoplayer-hls):
            //   val source = HlsMediaSource.Factory(DefaultDataSource.Factory(context))
            //       .createMediaSource(MediaItem.fromUri(Uri.fromFile(master)))
            //   exoPlayer.setMediaSource(source); exoPlayer.prepare()
        }

        override fun onFailure(error: HlsError) { /* handle error */ }

        override fun onProgress(rendition: Rendition, percent: Float) { /* update UI */ }

        override fun onCancelled() { /* cleanup */ }
    },
)

// To cancel:
HlsPreparer.cancel()

Integrating with an upload pipeline

Most real integrations transcode → upload each segment → rewrite the playlist filenames to URLs → publish the rewritten master. The library ships the pieces you need.

Shortcut: HlsUploadHelper

The uploader lambda returns HlsUploaded<T>(url, metadata) where T is a caller-chosen type. Use it to thread per-upload data you need later — content hashes, sizes, server IDs, anything — without maintaining a side-channel map. Callers who only want URLs use T = Unit.

import com.davotoula.lightcompressor.hls.HlsConfig
import com.davotoula.lightcompressor.hls.HlsContentTypes
import com.davotoula.lightcompressor.hls.HlsUploaded
import com.davotoula.lightcompressor.hls.HlsUploadHelper
import com.davotoula.lightcompressor.hls.HlsUploadResult

data class UploadedBlob(val sha256: String, val sizeBytes: Long)

suspend fun uploadHls(context: Context, videoUri: Uri): String {
    val result: HlsUploadResult<UploadedBlob> = HlsUploadHelper.run(
        context = context,
        uri = videoUri,
        config = HlsConfig(),
    ) { file, suggestedFilename ->
        // Invoked on Dispatchers.IO. Upload the file and return the URL + any metadata
        // you need downstream (e.g. for NIP-71 `imeta` tags, signed manifests, analytics).
        val contentType = if (suggestedFilename.endsWith(".m3u8")) {
            HlsContentTypes.HLS_PLAYLIST
        } else {
            HlsContentTypes.FMP4_SEGMENT
        }
        val blob = myUploader.upload(file, filename = suggestedFilename, contentType = contentType)
        HlsUploaded(url = blob.url, metadata = UploadedBlob(blob.sha256, blob.sizeBytes))
    }

    // Look up any upload's metadata by the same identifiers the library already gave you:
    //   result.uploads[summary.combinedFilename] // single-file rendition
    //   result.uploads[summary.playlistFilename] // media playlist
    //   result.uploads[rendition.suggestedFilename(segment)] // individual segment
    result.renditions.forEach { summary ->
        val renditionFile = summary.combinedFilename ?: return@forEach
        val blob = result.uploads[renditionFile]?.metadata ?: return@forEach
        println("${summary.rendition.resolution.label}: ${summary.width}x${summary.height}, sha256=${blob.sha256}, ${blob.sizeBytes} bytes")
    }

    // result.masterPlaylist is already rewritten to point at the uploaded URLs.
    // Publish it however you want — write to disk, post to your CDN, insert into a DB row…
    return myUploader.uploadText(
        content = result.masterPlaylist,
        filename = "master.m3u8",
        contentType = HlsContentTypes.HLS_PLAYLIST,
    ).url
}

If you only need URLs and don't want to define a metadata type, use Unit:

val result: HlsUploadResult<Unit> = HlsUploadHelper.run(ctx, videoUri) { file, name ->
    HlsUploaded(url = myUploader.upload(file).url, metadata = Unit)
}

HlsUploadResult.renditions carries per-rendition HlsRenditionSummary objects — use summary.width, summary.height, and summary.codecString to build downstream metadata (e.g. Nostr imeta tags) without re-parsing the master playlist. HlsUploadResult.uploads is a LinkedHashMap preserving the upload timeline: all segments first (across renditions, in emission order), then all media playlists (in rendition order). Map keys match exactly what rendition.suggestedFilename(segment), summary.playlistFilename, and summary.combinedFilename return — consumers can look up entries by the same identifiers they already hold.

Manual integration with PlaylistRewriter

If you need custom orchestration (retry, concurrency, progress reporting), wire the pieces yourself:

import com.davotoula.lightcompressor.hls.HlsRenditionSummary
import com.davotoula.lightcompressor.hls.PlaylistRewriter
import com.davotoula.lightcompressor.hls.SimpleHlsListener
import com.davotoula.lightcompressor.hls.suggestedFilename

class UploadingListener(private val uploader: Uploader) : SimpleHlsListener() {
    private val segmentUrls = mutableMapOf<Rendition, MutableMap<String, String>>()
    private val renditionPlaylistUrls = mutableMapOf<String, String>()

    override fun onSegmentReady(rendition: Rendition, segment: HlsSegment) {
        val key = rendition.suggestedFilename(segment)
        val url = uploader.upload(segment.file, filename = key)
        segmentUrls.getOrPut(rendition) { mutableMapOf() }[key] = url
    }

    override fun onRenditionComplete(rendition: Rendition, summary: HlsRenditionSummary) {
        // The media playlist references segments by bare filename (e.g. "init.mp4",
        // "segment_000.m4s"), while the rewrite map is keyed by the library's full
        // rendition.suggestedFilename(segment) output (e.g. "720p/init.mp4"). Strip the
        // "<label>/" prefix before rewriting so the keys match what the playlist actually
        // contains. Combined-rendition keys like "720p.mp4" have no prefix to strip.
        val prefix = "${rendition.resolution.label}/"
        val perRenditionMap =
            (segmentUrls[rendition] ?: emptyMap()).entries.associate { (key, url) ->
                (if (key.startsWith(prefix)) key.removePrefix(prefix) else key) to url
            }
        val rewritten = PlaylistRewriter.rewrite(
            playlist = summary.mediaPlaylist,
            urlMap = perRenditionMap,
        )
        val url = uploader.uploadText(rewritten, filename = summary.playlistFilename)
        renditionPlaylistUrls[summary.playlistFilename] = url
    }

    override fun onComplete(masterPlaylist: String) {
        val rewrittenMaster = PlaylistRewriter.rewrite(masterPlaylist, renditionPlaylistUrls)
        uploader.uploadText(rewrittenMaster, filename = "master.m3u8")
    }
}

Rewrite-map keys are whatever Rendition.suggestedFilename(segment) / HlsRenditionSummary.playlistFilename return — the library guarantees the two are consistent, so your rewrite map just needs to match its own lookups.

MIME types

.m3u8 playlists must be served as application/vnd.apple.mpegurl. Android's MimeTypeMap does not know this type, so if your upload pipeline derives content types from file extensions you must special-case it. Use HlsContentTypes.HLS_PLAYLIST / HlsContentTypes.FMP4_SEGMENT as the canonical source.

Per-variant metadata

HlsRenditionSummary carries width, height, codecString, playlistFilename, and combinedFilename. Consumers building manifest metadata (e.g. Nostr imeta tags with variant, dim, codec) should read these from the summary rather than regex-parsing the master playlist.

HLS playback on the consumer side requires the Media3 HLS module:

implementation "androidx.media3:media3-exoplayer-hls:$media3_version"

Sample App

The app/ module contains a Jetpack Compose sample app demonstrating the library. Install it via:

Google PlayObtaniumZapStore

Compatibility

Minimum Android SDK: API level 21

Attribution

Originally forked from AbedElazizShe/LightCompressor. Based on Telegram for Android.

License

Apache License 2.0

About

A powerful and easy-to-use video compression library and app (LCe) for android that uses the native MediaCodec API. It supports H.264/H2.65 hardware accelerated encoding and configurable resolution, bitrate, fast-start (streamable), and Gif to Mp4 conversion.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages