How to Read Android Assets in Mobile FFmpeg

This article explains how to use the Android Native Asset Manager within a mobile FFmpeg application to access and process media files stored in your project’s assets folder. Because FFmpeg requires standard file paths or protocols to read data, and Android assets are packaged inside the APK, developers must bridge this gap. You will learn the two main approaches to resolve this: extracting assets to a temporary cache directory, or implementing a custom FFmpeg native I/O protocol using the Android NDK AAssetManager.


The Challenge of Android Assets in FFmpeg

FFmpeg natively expects standard file system paths (like /sdcard/video.mp4 or /data/data/...) or stream protocols. However, files placed in the src/main/assets folder of an Android project are compressed inside the APK (or AAB) and do not have an absolute file path.

To feed these assets into FFmpeg, you must either temporarily extract the file to the local disk or pipe the binary data directly to FFmpeg using native code.


The easiest and most reliable way to read an asset with FFmpeg is to copy the asset file into your application’s internal cache directory and pass the resulting file path to FFmpeg.

1. Copy the Asset to Cache in Java/Kotlin

fun getAssetCachePath(context: Context, assetName: String): String {
    val cacheFile = File(context.cacheDir, assetName)
    if (!cacheFile.exists()) {
        context.assets.open(assetName).use { inputStream ->
            FileOutputStream(cacheFile).use { outputStream ->
                inputStream.copyTo(outputStream)
            }
        }
    }
    return cacheFile.absolutePath
}

2. Pass the Path to FFmpeg

Once you have the absolute path, you can run your FFmpeg command as normal:

val inputPath = getAssetCachePath(context, "my_video.mp4")
val outputPath = "${context.filesDir}/output.gif"

val cmd = "-i $inputPath -vf scale=320:-1 $outputPath"
// Execute cmd using your mobile FFmpeg library (e.g., FFmpegKit)

Pros: Extremely simple, works with any FFmpeg mobile library, and supports formats that require seeking (like MP4).
Cons: Requires additional storage space and adds write latency for large files.


Method 2: Native NDK Asset Manager and Custom AVIOContext (Direct)

For advanced performance where you want to avoid duplicating files on disk, you can pass Android’s native AssetManager to C++ and write a custom FFmpeg AVIOContext. This reads data directly from the APK using memory buffers.

1. Pass the Asset Manager to JNI

Pass your Android context’s AssetManager to your native C++ code through JNI:

external fun processAsset(assetManager: AssetManager, assetName: String)

2. Native C++ JNI Setup

In your native code, include the Android NDK asset manager headers and obtain the AAssetManager pointer:

#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>

extern "C"
JNIEXPORT void JNICALL
Java_com_example_app_MainActivity_processAsset(JNIEnv *env, jobject thiz, jobject assetManager, jstring assetName) {
    const char *name = env->GetStringUTFChars(assetName, nullptr);
    AAssetManager *mgr = AAssetManager_fromJava(env, assetManager);
    AAsset *asset = AAssetManager_open(mgr, name, AASSET_MODE_RANDOM);
    
    if (!asset) {
        // Handle error
        env->ReleaseStringUTFChars(assetName, name);
        return;
    }

    // Set up AVIO custom context
    size_t buffer_size = 4096;
    unsigned char *avio_buffer = (unsigned char *)av_malloc(buffer_size);
    
    AVIOContext *avio_ctx = avio_alloc_context(
        avio_buffer, buffer_size,
        0,                  // Write flag (0 = read-only)
        asset,              // User data passed to callbacks
        read_packet,        // Custom read callback
        nullptr,            // Custom write callback
        seek_packet         // Custom seek callback
    );

    AVFormatContext *fmt_ctx = avformat_alloc_context();
    fmt_ctx->pb = avio_ctx;

    // Open input using the custom I/O context
    if (avformat_open_input(&fmt_ctx, "", nullptr, nullptr) == 0) {
        // FFmpeg is now reading directly from the Android Asset
        avformat_find_stream_info(fmt_ctx, nullptr);
        
        // Add your FFmpeg processing logic here...

        avformat_close_input(&fmt_ctx);
    }

    // Cleanup
    av_freep(&avio_ctx->buffer);
    avio_context_free(&avio_ctx);
    AAsset_close(asset);
    env->ReleaseStringUTFChars(assetName, name);
}

3. Implement Custom Callback Functions

FFmpeg needs to know how to read and seek inside the AAsset. Implement the read_packet and seek_packet callbacks using the NDK API:

int read_packet(void *opaque, uint8_t *buf, int buf_size) {
    AAsset *asset = static_cast<AAsset *>(opaque);
    int read_bytes = AAsset_read(asset, buf, buf_size);
    if (read_bytes == 0) {
        return AVERROR_EOF;
    }
    return read_bytes;
}

int64_t seek_packet(void *opaque, int64_t offset, int whence) {
    AAsset *asset = static_cast<AAsset *>(opaque);
    
    // Convert FFmpeg seek flags to Android NDK seek flags
    int seek_whence = SEEK_SET;
    if (whence == AVSEEK_SIZE) {
        return AAsset_getLength64(asset);
    } else if (whence == SEEK_CUR) {
        seek_whence = SEEK_CUR;
    } else if (whence == SEEK_END) {
        seek_whence = SEEK_END;
    }

    off64_t result = AAsset_seek64(asset, offset, seek_whence);
    if (result < 0) {
        return -1;
    }
    return result;
}

Pros: Zero-copy operations, saves disk space, and avoids write-to-disk latency.
Cons: Requires writing custom C++ JNI code and manually building the NDK toolchain alongside your FFmpeg dependency.