Android使用FFmpeg實(shí)現(xiàn)視頻解碼的全流程指南
一、架構(gòu)設(shè)計(jì)
1.1 整體架構(gòu)
采用三層架構(gòu)設(shè)計(jì):
• 應(yīng)用層:提供用戶接口和UI展示
• 業(yè)務(wù)邏輯層:管理解碼流程和狀態(tài)
• Native層:FFmpeg核心解碼實(shí)現(xiàn)
1.2 狀態(tài)管理方案
使用靜態(tài)常量替代枚舉類:
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; }
二、核心類實(shí)現(xiàn)
2.1 視頻幀數(shù)據(jù)封裝類
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) { this.videoData = videoData; this.width = width; this.height = height; this.pts = pts; this.format = format; this.rotation = rotation; } // Getter方法 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; } // 轉(zhuǎn)換為Bitmap public Bitmap toBitmap() { YuvImage yuvImage = new YuvImage(videoData, ImageFormat.NV21, width, height, null); ByteArrayOutputStream os = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, os); byte[] jpegByteArray = os.toByteArray(); Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, 0, jpegByteArray.length); // 處理旋轉(zhuǎn) if (rotation != 0) { Matrix matrix = new Matrix(); matrix.postRotate(rotation); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } return bitmap; } }
2.2 視頻解碼器封裝類
public class VideoDecoder { // 解碼狀態(tài)常量 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; // 錯(cuò)誤碼常量 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(Looper.getMainLooper()); } 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; mainHandler.post(() -> { if (listener != null) { listener.onStateChanged(newState); } }); } private void notifyError(int errorCode, String message) { mainHandler.post(() -> { if (listener != null) { listener.onErrorOccurred(errorCode, message); } }); } // Native方法 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 { System.loadLibrary("avcodec"); System.loadLibrary("avformat"); System.loadLibrary("avutil"); System.loadLibrary("swscale"); System.loadLibrary("ffmpeg-wrapper"); } }
三、Native層實(shí)現(xiàn)
3.1 上下文結(jié)構(gòu)體
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接口實(shí)現(xiàn)
// 初始化解碼器 JNIEXPORT 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; } // 準(zhǔn)備解碼器 JNIEXPORT 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); // 打開媒體文件 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; } // 獲取流信息 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; } // 查找視頻流 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; // 獲取視頻旋轉(zhuǎn)信息 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; } } // 檢查是否找到視頻流 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; } // 獲取解碼器參數(shù) 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; } // 創(chuàng)建解碼上下文 ctx->codec_ctx = avcodec_alloc_context3(decoder); avcodec_parameters_to_context(ctx->codec_ctx, codec_params); // 打開解碼器 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; } // 保存視頻尺寸 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 核心解碼邏輯
// 開始解碼 JNIEXPORT 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; // 獲取Java回調(diào)方法和類 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"); // 分配幀和包 AVFrame *frame = av_frame_alloc(); AVFrame *rgb_frame = av_frame_alloc(); AVPacket *packet = av_packet_alloc(); // 準(zhǔn)備圖像轉(zhuǎn)換上下文 (轉(zhuǎn)換為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; } // 分配RGB緩沖區(qū) 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); // 解碼循環(huán) while (ctx->is_decoding && av_read_frame(ctx->format_ctx, packet) >= 0) { if (packet->stream_index == ctx->video_stream_idx) { // 發(fā)送到解碼器 if (avcodec_send_packet(ctx->codec_ctx, packet) == 0) { // 接收解碼后的幀 while (avcodec_receive_frame(ctx->codec_ctx, frame) == 0) { if (!ctx->is_decoding) break; // 等待暫停狀態(tài)結(jié)束 while (ctx->is_paused && ctx->is_decoding) { usleep(10000); // 10ms } if (!ctx->is_decoding) break; // 轉(zhuǎn)換像素格式 sws_scale(ctx->sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, 0, ctx->video_height, rgb_frame->data, rgb_frame->linesize); // 創(chuàng)建Java VideoFrame對(duì)象 jclass frame_class = (*env)->FindClass(env, "com/example/VideoFrame"); jmethodID frame_ctor = (*env)->GetMethodID(env, frame_class, "<init>", "([BIIJI)V"); // 創(chuàng)建字節(jié)數(shù)組 jbyteArray rgb_array = (*env)->NewByteArray(env, rgb_buffer_size); (*env)->SetByteArrayRegion(env, rgb_array, 0, rgb_buffer_size, (jbyte *)rgb_buffer); // 創(chuàng)建VideoFrame對(duì)象 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); // 回調(diào)到Java層 (*env)->CallVoidMethod(env, listener, on_frame_method, video_frame); // 釋放本地引用 (*env)->DeleteLocalRef(env, video_frame); (*env)->DeleteLocalRef(env, rgb_array); } } } av_packet_unref(packet); } // 解碼完成回調(diào) if (ctx->is_decoding) { (*env)->CallVoidMethod(env, listener, on_finish_method); } end: // 釋放資源 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); }
四、使用示例
public class VideoPlayerActivity extends AppCompatActivity implements VideoDecoder.DecodeListener { private VideoDecoder videoDecoder; private ImageView videoView; private Button btnPlay, btnPause, btnStop; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_video_player); videoView = findViewById(R.id.video_view); btnPlay = findViewById(R.id.btn_play); btnPause = findViewById(R.id.btn_pause); btnStop = findViewById(R.id.btn_stop); videoDecoder = new VideoDecoder(); // 準(zhǔn)備視頻文件 String videoPath = getExternalFilesDir(null) + "/test.mp4"; // 設(shè)置按鈕點(diǎn)擊監(jiān)聽 btnPlay.setOnClickListener(v -> { if (videoDecoder.getCurrentState() == VideoDecoder.STATE_READY || videoDecoder.getCurrentState() == VideoDecoder.STATE_PAUSED) { videoDecoder.startDecoding(this); } else if (videoDecoder.getCurrentState() == VideoDecoder.STATE_IDLE) { videoDecoder.prepare(videoPath); } }); btnPause.setOnClickListener(v -> { if (videoDecoder.getCurrentState() == VideoDecoder.STATE_DECODING) { videoDecoder.pause(); } }); btnStop.setOnClickListener(v -> { if (videoDecoder.getCurrentState() != VideoDecoder.STATE_IDLE && videoDecoder.getCurrentState() != VideoDecoder.STATE_STOPPED) { videoDecoder.stop(); } }); } @Override public void onFrameDecoded(VideoFrame frame) { runOnUiThread(() -> { Bitmap bitmap = frame.toBitmap(); videoView.setImageBitmap(bitmap); }); } @Override public void onDecodeFinished() { runOnUiThread(() -> { Toast.makeText(this, "解碼完成", Toast.LENGTH_SHORT).show(); videoView.setImageBitmap(null); }); } @Override public void onErrorOccurred(int errorCode, String message) { runOnUiThread(() -> { String errorMsg = "錯(cuò)誤(" + errorCode + "): " + message; Toast.makeText(this, errorMsg, Toast.LENGTH_LONG).show(); }); } @Override public void onStateChanged(int newState) { runOnUiThread(() -> updateUI(newState)); } private void updateUI(int state) { btnPlay.setEnabled(state == VideoDecoder.STATE_READY || state == VideoDecoder.STATE_PAUSED || state == VideoDecoder.STATE_IDLE); btnPause.setEnabled(state == VideoDecoder.STATE_DECODING); btnStop.setEnabled(state == VideoDecoder.STATE_DECODING || state == VideoDecoder.STATE_PAUSED); } @Override protected void onDestroy() { super.onDestroy(); videoDecoder.release(); } }
五、性能優(yōu)化建議
使用Surface直接渲染:
• 通過ANativeWindow直接渲染YUV數(shù)據(jù),避免格式轉(zhuǎn)換
• 減少內(nèi)存拷貝和Bitmap創(chuàng)建開銷
硬解碼優(yōu)先:
// 在nativePrepare中檢測(cè)硬件解碼器 AVCodec *decoder = NULL; if (isHardwareDecodeSupported(codec_id)) { decoder = avcodec_find_decoder_by_name("h264_mediacodec"); } if (!decoder) { decoder = avcodec_find_decoder(codec_id); }
幀緩沖隊(duì)列優(yōu)化:
• 實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模型
• 設(shè)置合理的隊(duì)列大小(3-5幀)
• 丟幀策略處理視頻不同步問題
多線程處理:
• 分離解碼線程和渲染線程
• 使用線程池處理耗時(shí)操作
內(nèi)存復(fù)用:
// 復(fù)用AVPacket和AVFrame static AVPacket *reuse_packet = NULL; if (!reuse_packet) { reuse_packet = av_packet_alloc(); } else { av_packet_unref(reuse_packet); }
精準(zhǔn)幀率控制:
// 根據(jù)幀率控制解碼速度 AVRational frame_rate = ctx->format_ctx->streams[ctx->video_stream_idx]->avg_frame_rate; double frame_delay = av_q2d(av_inv_q(frame_rate)) * 1000000; // 微秒 int64_t last_frame_time = av_gettime(); while (decoding) { // ...解碼邏輯... 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(); }
低功耗優(yōu)化:
• 根據(jù)設(shè)備溫度調(diào)整解碼策略
• 在后臺(tái)時(shí)降低幀率或暫停解碼
六、兼容性處理
API版本適配:
private static boolean isSurfaceTextureSupported() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; }
權(quán)限處理:
private boolean checkStoragePermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } return true; }
ABI兼容:
android { defaultConfig { ndk { abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' } } }
七、錯(cuò)誤處理與日志
完善的錯(cuò)誤處理:
public void onErrorOccurred(int errorCode, String message) { switch (errorCode) { case VideoDecoder.ERROR_CODE_FILE_NOT_FOUND: // 處理文件不存在錯(cuò)誤 break; case VideoDecoder.ERROR_CODE_UNSUPPORTED_FORMAT: // 處理不支持的格式錯(cuò)誤 break; default: // 處理未知錯(cuò)誤 } }
日志系統(tǒng):
#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); } }
八、擴(kuò)展功能
視頻信息獲?。?/p>
public class VideoInfo { public int width; public int height; public long duration; public float frameRate; public int rotation; } // 在VideoDecoder中添加方法 public VideoInfo getVideoInfo() { return nativeGetVideoInfo(nativeHandle); }
視頻截圖功能:
public Bitmap captureFrame() { if (currentState == STATE_DECODING || currentState == STATE_PAUSED) { return nativeCaptureFrame(nativeHandle); } return null; }
視頻縮放控制:
// 在native層實(shí)現(xiàn)縮放 sws_scale(ctx->sws_ctx, frame->data, frame->linesize, 0, ctx->video_height, scaled_frame->data, scaled_frame->linesize);
九、測(cè)試建議
單元測(cè)試:
@Test public void testDecoderStates() { VideoDecoder decoder = new VideoDecoder(); assertEquals(VideoDecoder.STATE_IDLE, decoder.getCurrentState()); decoder.prepare("test.mp4"); // 等待準(zhǔn)備完成 assertEquals(VideoDecoder.STATE_READY, decoder.getCurrentState()); }
性能測(cè)試:
long startTime = System.currentTimeMillis(); // 執(zhí)行解碼操作 long endTime = System.currentTimeMillis(); Log.d("Performance", "解碼耗時(shí): " + (endTime - startTime) + "ms");
內(nèi)存泄漏檢測(cè):
• 使用Android Profiler監(jiān)控內(nèi)存使用
• 重復(fù)創(chuàng)建釋放解碼器檢查內(nèi)存增長(zhǎng)
十、總結(jié)
本文實(shí)現(xiàn)的Android FFmpeg視頻解碼方案具有以下特點(diǎn):
- 高性能:通過Native層優(yōu)化和合理的內(nèi)存管理實(shí)現(xiàn)高效解碼
- 高兼容性:避免使用枚舉類,支持廣泛的Android設(shè)備
- 可擴(kuò)展性:模塊化設(shè)計(jì)便于添加新功能
- 穩(wěn)定性:完善的狀態(tài)管理和錯(cuò)誤處理機(jī)制
- 易用性:清晰的API接口和完整的文檔
開發(fā)者可以根據(jù)實(shí)際需求在此基礎(chǔ)框架上進(jìn)行擴(kuò)展,如添加音頻解碼、視頻濾鏡等功能,構(gòu)建更完整的媒體播放解決方案。
到此這篇關(guān)于Android使用FFmpeg實(shí)現(xiàn)視頻解碼的全流程指南的文章就介紹到這了,更多相關(guān)Android FFmpeg視頻解碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android錄音功能的實(shí)現(xiàn)以及踩坑實(shí)戰(zhàn)記錄
在Android 開發(fā)過程中,有些功能是通用的,或者是多個(gè)業(yè)務(wù)方都需要使用的,下面這篇文章主要給大家介紹了關(guān)于Android錄音功能的實(shí)現(xiàn)以及踩坑的相關(guān)資料,需要的朋友可以參考下2022-06-06Android自定義view系列之99.99%實(shí)現(xiàn)QQ側(cè)滑刪除效果實(shí)例代碼詳解
這篇文章給大家介紹android自定義view系列之99.99%實(shí)現(xiàn)QQ側(cè)滑刪除效果,本文介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友參考下吧2016-09-09Android Fragment的靜態(tài)注冊(cè)和動(dòng)態(tài)注冊(cè)創(chuàng)建步驟
這篇文章主要介紹了Android Fragment的靜態(tài)注冊(cè)和動(dòng)態(tài)注冊(cè)創(chuàng)建步驟,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Android的OkHttp包中的HTTP攔截器Interceptor用法示例
攔截器是OkHttp處理HTTP請(qǐng)求方面所具有的一個(gè)強(qiáng)大特性,這里我們就來看一下Android的OkHttp包中的HTTP攔截器Interceptor用法示例,需要的朋友可以參考下2016-07-07親自動(dòng)手實(shí)現(xiàn)Android App插件化
這篇文章主要教大家親自動(dòng)手實(shí)現(xiàn)Android App插件化,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12在Android中 獲取正在運(yùn)行的Service 實(shí)例
本篇文章小編為大家介紹,在Android中 獲取正在運(yùn)行的Service 實(shí)例。需要的朋友參考下2013-04-04