SoFunction
Updated on 2025-05-09

Android's full process guide to using FFmpeg to implement video decoding

1. Architectural design

1.1 Overall architecture

Adopt a three-layer architecture design:

• Application layer: Provide user interface and UI display

• Business logic layer: manage decoding process and status

• Native layer: FFmpeg core decoding implementation

1.2 Status Management Plan

Use static constants instead of enumeration classes:

public class DecodeState {
    public static final int STATE_IDLE = 0;
    public static final int STATE_PREPARING = 1;
    public static final int STATE_READY = 2;
    public static final int STATE_DECODING = 3;
    public static final int STATE_PAUSED = 4;
    public static final int STATE_STOPPED = 5;
    public static final int STATE_ERROR = 6;
}

2. Core class implementation

2.1 Video frame data encapsulation class

public class VideoFrame {
    private final byte[] videoData;
    private final int width;
    private final int height;
    private final long pts;
    private final int format;
    private final int rotation;
    
    public VideoFrame(byte[] videoData, int width, int height, long pts, int format, int rotation) {
         = videoData;
         = width;
         = height;
         = pts;
         = format;
         = rotation;
    }
    
    // Getter method    public byte[] getVideoData() {
        return videoData;
    }
    
    public int getWidth() {
        return width;
    }
    
    public int getHeight() {
        return height;
    }
    
    public long getPts() {
        return pts;
    }
    
    public int getFormat() {
        return format;
    }
    
    public int getRotation() {
        return rotation;
    }
    
    // Convert to Bitmap    public Bitmap toBitmap() {
        YuvImage yuvImage = new YuvImage(videoData, ImageFormat.NV21, width, height, null);
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        (new Rect(0, 0, width, height), 100, os);
        byte[] jpegByteArray = ();
        Bitmap bitmap = (jpegByteArray, 0, );
        
        // Handle rotation        if (rotation != 0) {
            Matrix matrix = new Matrix();
            (rotation);
            bitmap = (bitmap, 0, 0, 
                                      (), (), 
                                      matrix, true);
        }
        
        return bitmap;
    }
}

2.2 Video decoder packaging class

public class VideoDecoder {
    // Decode state constants    public static final int STATE_IDLE = 0;
    public static final int STATE_PREPARING = 1;
    public static final int STATE_READY = 2;
    public static final int STATE_DECODING = 3;
    public static final int STATE_PAUSED = 4;
    public static final int STATE_STOPPED = 5;
    public static final int STATE_ERROR = 6;
    
    // Error code constant    public static final int ERROR_CODE_FILE_NOT_FOUND = 1001;
    public static final int ERROR_CODE_UNSUPPORTED_FORMAT = 1002;
    public static final int ERROR_CODE_DECODE_FAILED = 1003;
    
    private volatile int currentState = STATE_IDLE;
    private long nativeHandle;
    private Handler mainHandler;
    
    public interface DecodeListener {
        void onFrameDecoded(VideoFrame frame);
        void onDecodeFinished();
        void onErrorOccurred(int errorCode, String message);
        void onStateChanged(int newState);
    }
    
    public VideoDecoder() {
        nativeHandle = nativeInit();
        mainHandler = new Handler(());
    }
    
    public void prepare(String filePath) {
        if (currentState != STATE_IDLE) {
            notifyError(ERROR_CODE_DECODE_FAILED, "Decoder is not in idle state");
            return;
        }
        
        setState(STATE_PREPARING);
        
        new Thread(() -> {
            boolean success = nativePrepare(nativeHandle, filePath);
            if (success) {
                setState(STATE_READY);
            } else {
                setState(STATE_ERROR);
                notifyError(ERROR_CODE_FILE_NOT_FOUND, "Failed to prepare decoder");
            }
        }).start();
    }
    
    public void startDecoding(DecodeListener listener) {
        if (currentState != STATE_READY && currentState != STATE_PAUSED) {
            notifyError(ERROR_CODE_DECODE_FAILED, "Decoder is not ready");
            return;
        }
        
        setState(STATE_DECODING);
        
        new Thread(() -> {
            nativeStartDecoding(nativeHandle, listener);
            setState(STATE_STOPPED);
        }).start();
    }
    
    public void pause() {
        if (currentState == STATE_DECODING) {
            setState(STATE_PAUSED);
            nativePause(nativeHandle);
        }
    }
    
    public void resume() {
        if (currentState == STATE_PAUSED) {
            setState(STATE_DECODING);
            nativeResume(nativeHandle);
        }
    }
    
    public void stop() {
        setState(STATE_STOPPED);
        nativeStop(nativeHandle);
    }
    
    public void release() {
        setState(STATE_STOPPED);
        nativeRelease(nativeHandle);
        nativeHandle = 0;
    }
    
    public int getCurrentState() {
        return currentState;
    }
    
    private void setState(int newState) {
        currentState = newState;
        (() -> {
            if (listener != null) {
                (newState);
            }
        });
    }
    
    private void notifyError(int errorCode, String message) {
        (() -> {
            if (listener != null) {
                (errorCode, message);
            }
        });
    }
    
    // Native method    private native long nativeInit();
    private native boolean nativePrepare(long handle, String filePath);
    private native void nativeStartDecoding(long handle, DecodeListener listener);
    private native void nativePause(long handle);
    private native void nativeResume(long handle);
    private native void nativeStop(long handle);
    private native void nativeRelease(long handle);
    
    static {
        ("avcodec");
        ("avformat");
        ("avutil");
        ("swscale");
        ("ffmpeg-wrapper");
    }
}

3. Native layer implementation

3.1 Context structure

typedef struct {
    AVFormatContext *format_ctx;
    AVCodecContext *codec_ctx;
    int video_stream_idx;
    SwsContext *sws_ctx;
    volatile int is_decoding;
    volatile int is_paused;
    int video_width;
    int video_height;
    int rotation;
} VideoDecodeContext;

3.2 JNI interface implementation

// Initialization decoderJNIEXPORT jlong JNICALL
Java_com_example_VideoDecoder_nativeInit(JNIEnv *env, jobject thiz) {
    VideoDecodeContext *ctx = (VideoDecodeContext *)malloc(sizeof(VideoDecodeContext));
    memset(ctx, 0, sizeof(VideoDecodeContext));
    ctx->is_decoding = 0;
    ctx->is_paused = 0;
    ctx->rotation = 0;
    return (jlong)ctx;
}

// Prepare the decoderJNIEXPORT jboolean JNICALL
Java_com_example_VideoDecoder_nativePrepare(JNIEnv *env, jobject thiz, 
                                          jlong handle, jstring file_path) {
    VideoDecodeContext *ctx = (VideoDecodeContext *)handle;
    const char *path = (*env)->GetStringUTFChars(env, file_path, NULL);
    
    // Open the media file    if (avformat_open_input(&ctx->format_ctx, path, NULL, NULL) != 0) {
        LOGE("Could not open file: %s", path);
        (*env)->ReleaseStringUTFChars(env, file_path, path);
        return JNI_FALSE;
    }
    
    // Get streaming information    if (avformat_find_stream_info(ctx->format_ctx, NULL) < 0) {
        LOGE("Could not find stream information");
        (*env)->ReleaseStringUTFChars(env, file_path, path);
        avformat_close_input(&ctx->format_ctx);
        return JNI_FALSE;
    }
    
    // Find video streams    ctx->video_stream_idx = -1;
    for (int i = 0; i < ctx->format_ctx->nb_streams; i++) {
        if (ctx->format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            ctx->video_stream_idx = i;
            
            // Get video rotation information            AVDictionaryEntry *rotate_tag = av_dict_get(ctx->format_ctx->streams[i]->metadata, 
                                                       "rotate", NULL, 0);
            if (rotate_tag && rotate_tag->value) {
                ctx->rotation = atoi(rotate_tag->value);
            }
            break;
        }
    }
    
    // Check whether the video stream is found    if (ctx->video_stream_idx == -1) {
        LOGE("Could not find video stream");
        (*env)->ReleaseStringUTFChars(env, file_path, path);
        avformat_close_input(&ctx->format_ctx);
        return JNI_FALSE;
    }
    
    // Get the decoder parameters    AVCodecParameters *codec_params = ctx->format_ctx->streams[ctx->video_stream_idx]->codecpar;
    AVCodec *decoder = avcodec_find_decoder(codec_params->codec_id);
    if (!decoder) {
        LOGE("Unsupported codec");
        (*env)->ReleaseStringUTFChars(env, file_path, path);
        avformat_close_input(&ctx->format_ctx);
        return JNI_FALSE;
    }
    
    // Create a decoding context    ctx->codec_ctx = avcodec_alloc_context3(decoder);
    avcodec_parameters_to_context(ctx->codec_ctx, codec_params);
    
    // Turn on the decoder    if (avcodec_open2(ctx->codec_ctx, decoder, NULL) < 0) {
        LOGE("Could not open codec");
        (*env)->ReleaseStringUTFChars(env, file_path, path);
        avcodec_free_context(&ctx->codec_ctx);
        avformat_close_input(&ctx->format_ctx);
        return JNI_FALSE;
    }
    
    // Save the video size    ctx->video_width = ctx->codec_ctx->width;
    ctx->video_height = ctx->codec_ctx->height;
    
    (*env)->ReleaseStringUTFChars(env, file_path, path);
    return JNI_TRUE;
}

3.3 Core decoding logic

// Start decodingJNIEXPORT void JNICALL
Java_com_example_VideoDecoder_nativeStartDecoding(JNIEnv *env, jobject thiz, 
                                                jlong handle, jobject listener) {
    VideoDecodeContext *ctx = (VideoDecodeContext *)handle;
    ctx->is_decoding = 1;
    ctx->is_paused = 0;
    
    // Get Java callback methods and classes    jclass listener_class = (*env)->GetObjectClass(env, listener);
    jmethodID on_frame_method = (*env)->GetMethodID(env, listener_class, 
                                                  "onFrameDecoded", 
                                                  "(Lcom/example/VideoFrame;)V");
    jmethodID on_finish_method = (*env)->GetMethodID(env, listener_class, 
                                                   "onDecodeFinished", "()V");
    jmethodID on_error_method = (*env)->GetMethodID(env, listener_class, 
                                                  "onErrorOccurred", "(ILjava/lang/String;)V");
    
    // Assign frames and packets    AVFrame *frame = av_frame_alloc();
    AVFrame *rgb_frame = av_frame_alloc();
    AVPacket *packet = av_packet_alloc();
    
    // Prepare the image conversion context (convert to RGB24)    ctx->sws_ctx = sws_getContext(
        ctx->video_width, ctx->video_height, ctx->codec_ctx->pix_fmt,
        ctx->video_width, ctx->video_height, AV_PIX_FMT_RGB24,
        SWS_BILINEAR, NULL, NULL, NULL);
    
    if (!ctx->sws_ctx) {
        (*env)->CallVoidMethod(env, listener, on_error_method, 
                             VideoDecoder.ERROR_CODE_DECODE_FAILED,
                             (*env)->NewStringUTF(env, "Could not initialize sws context"));
        goto end;
    }
    
    // Allocate RGB buffer    int rgb_buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGB24, 
                                                  ctx->video_width, 
                                                  ctx->video_height, 1);
    uint8_t *rgb_buffer = (uint8_t *)av_malloc(rgb_buffer_size);
    av_image_fill_arrays(rgb_frame->data, rgb_frame->linesize, rgb_buffer,
                        AV_PIX_FMT_RGB24, ctx->video_width, 
                        ctx->video_height, 1);
    
    // Decoding loop    while (ctx->is_decoding && av_read_frame(ctx->format_ctx, packet) >= 0) {
        if (packet->stream_index == ctx->video_stream_idx) {
            // Send to the decoder            if (avcodec_send_packet(ctx->codec_ctx, packet) == 0) {
                // Receive the decoded frame                while (avcodec_receive_frame(ctx->codec_ctx, frame) == 0) {
                    if (!ctx->is_decoding) break;
                    
                    // Wait for the pause status to end                    while (ctx->is_paused && ctx->is_decoding) {
                        usleep(10000); // 10ms
                    }
                    
                    if (!ctx->is_decoding) break;
                    
                    // Convert pixel format                    sws_scale(ctx->sws_ctx, (const uint8_t *const *)frame->data,
                             frame->linesize, 0, ctx->video_height,
                             rgb_frame->data, rgb_frame->linesize);
                    
                    // Create Java VideoFrame object                    jclass frame_class = (*env)->FindClass(env, "com/example/VideoFrame");
                    jmethodID frame_ctor = (*env)->GetMethodID(env, frame_class, 
                                                              "<init>", "([BIIJI)V");
                    
                    // Create byte array                    jbyteArray rgb_array = (*env)->NewByteArray(env, rgb_buffer_size);
                    (*env)->SetByteArrayRegion(env, rgb_array, 0, rgb_buffer_size, 
                                             (jbyte *)rgb_buffer);
                    
                    // Create VideoFrame object                    jobject video_frame = (*env)->NewObject(env, frame_class, frame_ctor,
                                                          rgb_array, 
                                                          ctx->video_width,
                                                          ctx->video_height,
                                                          frame->pts,
                                                          AV_PIX_FMT_RGB24,
                                                          ctx->rotation);
                    
                    // Callback to Java layer                    (*env)->CallVoidMethod(env, listener, on_frame_method, video_frame);
                    
                    // Release local reference                    (*env)->DeleteLocalRef(env, video_frame);
                    (*env)->DeleteLocalRef(env, rgb_array);
                }
            }
        }
        av_packet_unref(packet);
    }
    
    // Decoding completes callback    if (ctx->is_decoding) {
        (*env)->CallVoidMethod(env, listener, on_finish_method);
    }
    
end:
    // Free up resources    if (rgb_buffer) av_free(rgb_buffer);
    if (ctx->sws_ctx) sws_freeContext(ctx->sws_ctx);
    av_frame_free(&frame);
    av_frame_free(&rgb_frame);
    av_packet_free(&packet);
}

IV. Use examples

public class VideoPlayerActivity extends AppCompatActivity 
    implements  {
    
    private VideoDecoder videoDecoder;
    private ImageView videoView;
    private Button btnPlay, btnPause, btnStop;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        (savedInstanceState);
        setContentView(.activity_video_player);
        
        videoView = findViewById(.video_view);
        btnPlay = findViewById(.btn_play);
        btnPause = findViewById(.btn_pause);
        btnStop = findViewById(.btn_stop);
        
        videoDecoder = new VideoDecoder();
        
        // Prepare video files        String videoPath = getExternalFilesDir(null) + "/test.mp4";
        
        // Click the monitor for the settings button        (v -> {
            if (() == VideoDecoder.STATE_READY || 
                () == VideoDecoder.STATE_PAUSED) {
                (this);
            } else if (() == VideoDecoder.STATE_IDLE) {
                (videoPath);
            }
        });
        
        (v -> {
            if (() == VideoDecoder.STATE_DECODING) {
                ();
            }
        });
        
        (v -> {
            if (() != VideoDecoder.STATE_IDLE && 
                () != VideoDecoder.STATE_STOPPED) {
                ();
            }
        });
    }
    
    @Override
    public void onFrameDecoded(VideoFrame frame) {
        runOnUiThread(() -> {
            Bitmap bitmap = ();
            (bitmap);
        });
    }
    
    @Override
    public void onDecodeFinished() {
        runOnUiThread(() -> {
            (this, "Decoding is complete", Toast.LENGTH_SHORT).show();
            (null);
        });
    }
    
    @Override
    public void onErrorOccurred(int errorCode, String message) {
        runOnUiThread(() -> {
            String errorMsg = "mistake(" + errorCode + "): " + message;
            (this, errorMsg, Toast.LENGTH_LONG).show();
        });
    }
    
    @Override
    public void onStateChanged(int newState) {
        runOnUiThread(() -> updateUI(newState));
    }
    
    private void updateUI(int state) {
        (state == VideoDecoder.STATE_READY || 
                         state == VideoDecoder.STATE_PAUSED ||
                         state == VideoDecoder.STATE_IDLE);
        
        (state == VideoDecoder.STATE_DECODING);
        (state == VideoDecoder.STATE_DECODING || 
                         state == VideoDecoder.STATE_PAUSED);
    }
    
    @Override
    protected void onDestroy() {
        ();
        ();
    }
}

5. Performance optimization suggestions

Render directly using Surface:

• Direct rendering of YUV data through ANativeWindow to avoid format conversion

• Reduce memory copy and Bitmap creation overhead

Hard decoding is preferred:

// Detect hardware decoder in nativePrepareAVCodec *decoder = NULL;
if (isHardwareDecodeSupported(codec_id)) {
    decoder = avcodec_find_decoder_by_name("h264_mediacodec");
}
if (!decoder) {
    decoder = avcodec_find_decoder(codec_id);
}

Framebuffer queue optimization:

• Implementing the producer-consumer model

• Set a reasonable queue size (3-5 frames)

• Frame drop strategy deals with video out-synchronization problem

Multithreaded processing:

• Separate decoding and rendering threads

• Use thread pool to process time-consuming operations

Memory multiplexing:

// Reuse AVPacket and AVFramestatic AVPacket *reuse_packet = NULL;
if (!reuse_packet) {
    reuse_packet = av_packet_alloc();
} else {
    av_packet_unref(reuse_packet);
}

Precise frame rate control:

//Control the decoding speed according to the frame rateAVRational frame_rate = ctx->format_ctx->streams[ctx->video_stream_idx]->avg_frame_rate;
double frame_delay = av_q2d(av_inv_q(frame_rate)) * 1000000; // Microseconds
int64_t last_frame_time = av_gettime();
while (decoding) {
    // ...Decoding logic...    
    int64_t current_time = av_gettime();
    int64_t elapsed = current_time - last_frame_time;
    if (elapsed < frame_delay) {
        usleep(frame_delay - elapsed);
    }
    last_frame_time = av_gettime();
}

Low power optimization:

• Adjust the decoding strategy according to the device temperature

• Reduce frame rate or pause decoding in background

6. Compatibility processing

API version adaptation:

private static boolean isSurfaceTextureSupported() {
    return .SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
}

Permission processing:

private boolean checkStoragePermission() {
    if (.SDK_INT >= Build.VERSION_CODES.M) {
        return checkSelfPermission(.READ_EXTERNAL_STORAGE) 
               == PackageManager.PERMISSION_GRANTED;
    }
    return true;
}

ABI compatible:

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }
}

7. Error handling and logging

Perfect error handling:

public void onErrorOccurred(int errorCode, String message) {
    switch (errorCode) {
        case VideoDecoder.ERROR_CODE_FILE_NOT_FOUND:
            // There is no error in processing the file            break;
        case VideoDecoder.ERROR_CODE_UNSUPPORTED_FORMAT:
            // Handle unsupported format errors            break;
        default:
            // Handle unknown errors    }
}

Log system:

#define LOG_LEVEL_VERBOSE 1
#define LOG_LEVEL_DEBUG   2
#define LOG_LEVEL_INFO    3
#define LOG_LEVEL_WARN    4
#define LOG_LEVEL_ERROR   5

void log_print(int level, const char *tag, const char *fmt, ...) {
    if (level >= CURRENT_LOG_LEVEL) {
        va_list args;
        va_start(args, fmt);
        __android_log_vprint(level, tag, fmt, args);
        va_end(args);
    }
}

8. Extended functions

Video information acquisition:

public class VideoInfo {
    public int width;
    public int height;
    public long duration;
    public float frameRate;
    public int rotation;
}

// Add method in VideoDecoderpublic VideoInfo getVideoInfo() {
    return nativeGetVideoInfo(nativeHandle);
}

Video screenshot function:

public Bitmap captureFrame() {
    if (currentState == STATE_DECODING || currentState == STATE_PAUSED) {
        return nativeCaptureFrame(nativeHandle);
    }
    return null;
}

Video zoom control:

// Implement scaling in native layersws_scale(ctx->sws_ctx, frame->data, frame->linesize, 
         0, ctx->video_height, 
         scaled_frame->data, scaled_frame->linesize);

9. Test suggestions

Unit Tests:

@Test
public void testDecoderStates() {
    VideoDecoder decoder = new VideoDecoder();
    assertEquals(VideoDecoder.STATE_IDLE, ());
    
    ("test.mp4");
    // Wait for preparation to be completed    assertEquals(VideoDecoder.STATE_READY, ());
}

Performance Test:

long startTime = ();
// Perform decoding operationslong endTime = ();
("Performance", "Decoding time consuming: " + (endTime - startTime) + "ms");

Memory Leak Detection:

• Use Android Profiler to monitor memory usage

• Repeat creation of free decoder to check memory growth

10. Summary

The Android FFmpeg video decoding solution implemented in this article has the following characteristics:

  • High performance: Efficient decoding through Native layer optimization and reasonable memory management
  • High compatibility: Avoid enumeration classes, support a wide range of Android devices
  • Scalability: Modular design facilitates the addition of new features
  • Stability: Complete state management and error handling mechanism
  • Ease of use: clear API interface and complete documentation

Developers can expand on this basic framework according to actual needs, such as adding audio decoding, video filters and other functions to build a more complete media playback solution.

This is the article about the full process guide for Android using FFmpeg to implement video decoding. For more related content on Android FFmpeg video decoding, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!