Android使用FFmpeg實現(xiàn)視頻解碼的全流程指南
一、架構(gòu)設(shè)計
1.1 整體架構(gòu)
采用三層架構(gòu)設(shè)計:
• 應(yīng)用層:提供用戶接口和UI展示
• 業(yè)務(wù)邏輯層:管理解碼流程和狀態(tài)
• Native層:FFmpeg核心解碼實現(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;
}
二、核心類實現(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;
// 錯誤碼常量
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層實現(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接口實現(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;
}
// 準備解碼器
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();
// 準備圖像轉(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對象
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對象
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();
// 準備視頻文件
String videoPath = getExternalFilesDir(null) + "/test.mp4";
// 設(shè)置按鈕點擊監(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 = "錯誤(" + 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中檢測硬件解碼器
AVCodec *decoder = NULL;
if (isHardwareDecodeSupported(codec_id)) {
decoder = avcodec_find_decoder_by_name("h264_mediacodec");
}
if (!decoder) {
decoder = avcodec_find_decoder(codec_id);
}
幀緩沖隊列優(yōu)化:
• 實現(xiàn)生產(chǎn)者-消費者模型
• 設(shè)置合理的隊列大小(3-5幀)
• 丟幀策略處理視頻不同步問題
多線程處理:
• 分離解碼線程和渲染線程
• 使用線程池處理耗時操作
內(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);
}
精準幀率控制:
// 根據(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)整解碼策略
• 在后臺時降低幀率或暫停解碼
六、兼容性處理
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'
}
}
}
七、錯誤處理與日志
完善的錯誤處理:
public void onErrorOccurred(int errorCode, String message) {
switch (errorCode) {
case VideoDecoder.ERROR_CODE_FILE_NOT_FOUND:
// 處理文件不存在錯誤
break;
case VideoDecoder.ERROR_CODE_UNSUPPORTED_FORMAT:
// 處理不支持的格式錯誤
break;
default:
// 處理未知錯誤
}
}
日志系統(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);
}
}
八、擴展功能
視頻信息獲?。?/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層實現(xiàn)縮放
sws_scale(ctx->sws_ctx, frame->data, frame->linesize,
0, ctx->video_height,
scaled_frame->data, scaled_frame->linesize);
九、測試建議
單元測試:
@Test
public void testDecoderStates() {
VideoDecoder decoder = new VideoDecoder();
assertEquals(VideoDecoder.STATE_IDLE, decoder.getCurrentState());
decoder.prepare("test.mp4");
// 等待準備完成
assertEquals(VideoDecoder.STATE_READY, decoder.getCurrentState());
}
性能測試:
long startTime = System.currentTimeMillis();
// 執(zhí)行解碼操作
long endTime = System.currentTimeMillis();
Log.d("Performance", "解碼耗時: " + (endTime - startTime) + "ms");
內(nèi)存泄漏檢測:
• 使用Android Profiler監(jiān)控內(nèi)存使用
• 重復(fù)創(chuàng)建釋放解碼器檢查內(nèi)存增長
十、總結(jié)
本文實現(xiàn)的Android FFmpeg視頻解碼方案具有以下特點:
- 高性能:通過Native層優(yōu)化和合理的內(nèi)存管理實現(xiàn)高效解碼
- 高兼容性:避免使用枚舉類,支持廣泛的Android設(shè)備
- 可擴展性:模塊化設(shè)計便于添加新功能
- 穩(wěn)定性:完善的狀態(tài)管理和錯誤處理機制
- 易用性:清晰的API接口和完整的文檔
開發(fā)者可以根據(jù)實際需求在此基礎(chǔ)框架上進行擴展,如添加音頻解碼、視頻濾鏡等功能,構(gòu)建更完整的媒體播放解決方案。
到此這篇關(guān)于Android使用FFmpeg實現(xiàn)視頻解碼的全流程指南的文章就介紹到這了,更多相關(guān)Android FFmpeg視頻解碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android錄音功能的實現(xiàn)以及踩坑實戰(zhàn)記錄
在Android 開發(fā)過程中,有些功能是通用的,或者是多個業(yè)務(wù)方都需要使用的,下面這篇文章主要給大家介紹了關(guān)于Android錄音功能的實現(xiàn)以及踩坑的相關(guān)資料,需要的朋友可以參考下2022-06-06
Android自定義view系列之99.99%實現(xiàn)QQ側(cè)滑刪除效果實例代碼詳解
這篇文章給大家介紹android自定義view系列之99.99%實現(xiàn)QQ側(cè)滑刪除效果,本文介紹的非常詳細,具有參考借鑒價值,需要的朋友參考下吧2016-09-09
Android Fragment的靜態(tài)注冊和動態(tài)注冊創(chuàng)建步驟
這篇文章主要介紹了Android Fragment的靜態(tài)注冊和動態(tài)注冊創(chuàng)建步驟,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02
Android的OkHttp包中的HTTP攔截器Interceptor用法示例
攔截器是OkHttp處理HTTP請求方面所具有的一個強大特性,這里我們就來看一下Android的OkHttp包中的HTTP攔截器Interceptor用法示例,需要的朋友可以參考下2016-07-07

