NDK開發——FFmpeg視訊解碼

鋸齒流沙發表於2017-12-26

目前很多視訊專案,包括直播專案都是使用FFmpeg來進行視訊解碼,所以視訊解碼是學習FFmpeg的重點之一。

上一篇文章《雲伺服器Ubuntu下搭建NDK環境,並編譯FFmpeg》,已經編譯好了FFmpeg。這篇文章將使用編譯好的so庫實現視訊解碼。

該實踐專案實現功能:將本地的一個視訊檔案解碼,解碼之後儲存到本地。

1、搭建DNK專案

搭建NDK專案,之前有介紹過,如果使用Eclipse搭建的,可以參考這篇文章《Eclipse下搭建Android的NDK開發環境》,如果使用AndroidStudio搭建的,可以參考這篇文章《Android Studio搭建ndk開發流程》,本例將使用Eclipse搭建。

2、將編譯好的so庫和include檔案複製到JNI目錄下

視訊解碼.png

3、建立本地方法並生成標頭檔案

public class VideoUtils {

	public native static void decode(String input,String output);
	
	static{
		System.loadLibrary("avutil-54");
		System.loadLibrary("swresample-1");
		System.loadLibrary("avcodec-56");
		System.loadLibrary("avformat-56");
		System.loadLibrary("swscale-3");
		System.loadLibrary("postproc-53");
		System.loadLibrary("avfilter-5");
		System.loadLibrary("avdevice-56");
		System.loadLibrary("myffmpeg");
	}
}
複製程式碼

視訊解碼.png

上圖中新建了一個ffmpagePlayer.c檔案,用於實現標頭檔案的函式。

4、修改Android.mk

LOCAL_PATH := $(call my-dir)

#ffmpeg lib
include $(CLEAR_VARS)
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := libavcodec-56.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avdevice
LOCAL_SRC_FILES := libavdevice-56.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avfilter
LOCAL_SRC_FILES := libavfilter-5.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := libavformat-56.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := libavutil-54.so
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := libpostproc-53.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := libswresample-1.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := libswscale-3.so
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)

LOCAL_MODULE    := myffmpeg
LOCAL_SRC_FILES := ffmpagePlayer.c
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_LDLIBS := -llog #log
LOCAL_SHARED_LIBRARIES := avcodec avdevice avfilter avformat avutil postproc swresample swscale

include $(BUILD_SHARED_LIBRARY)
複製程式碼

5、新建Application.mk檔案

視訊解碼.png

APP_ABI := armeabi armeabi-v7a #必須指定生成mip64架構的so檔案,否則出錯
APP_PLATFORM := android-8
複製程式碼

6、實現視訊解碼

ffmpagePlayer.c檔案中標頭檔案函式(實現視訊解碼)。

#include "com_example_codecpro_VideoUtils.h"
#include <jni.h>

#include <android/log.h>
//編碼
#include "include/libavcodec/avcodec.h"
//封裝格式處理
#include "include/libavformat/avformat.h"
//畫素處理
#include "include/libswscale/swscale.h"

//巨集定義
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"tag",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"tag",FORMAT,##__VA_ARGS__);

JNIEXPORT void JNICALL Java_com_example_codecpro_VideoUtils_decode
(JNIEnv *env, jclass jcls, jstring input_jstr, jstring output_jstr){

	//需要轉碼的視訊檔案(輸入的視訊檔案)
	const char* input_cstr = (*env)->GetStringUTFChars(env,input_jstr,NULL);
	const char* output_cstr = (*env)->GetStringUTFChars(env,output_jstr,NULL);

	//1.註冊元件
	av_register_all();

	//封裝格式上下文
	AVFormatContext *pFormatCtx = avformat_alloc_context();

	//2.開啟輸入視訊檔案avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
	if(avformat_open_input(&pFormatCtx,input_cstr,NULL,NULL) != 0){
		LOGE("%s","開啟輸入視訊檔案失敗");
		return;
	}
	//3.獲取視訊資訊
	if(avformat_find_stream_info(pFormatCtx,NULL) < 0){
		LOGE("%s","獲取視訊資訊失敗");
		return;
	}

	//視訊解碼,需要找到視訊對應的AVStream所在pFormatCtx->streams的索引位置
	int video_stream_idx = -1;
	int i = 0;
	for(; i < pFormatCtx->nb_streams;i++){
		//根據型別判斷,是否是視訊流
		if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
			video_stream_idx = i;
			break;
		}
	}

	//4.獲取視訊解碼器
	AVCodecContext *pCodeCtx = pFormatCtx->streams[video_stream_idx]->codec;
	AVCodec *pCodec = avcodec_find_decoder(pCodeCtx->codec_id);
	if(pCodec == NULL){
		LOGE("%s","無法解碼");
		return;
	}

	//5.開啟解碼器
	if(avcodec_open2(pCodeCtx,pCodec,NULL) < 0){
		LOGE("%s","解碼器無法開啟");
		return;
	}

	//編碼資料
	AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));

	//畫素資料(解碼資料)
	AVFrame *frame = av_frame_alloc();
	AVFrame *yuvFrame = av_frame_alloc();

	//只有指定了AVFrame的畫素格式、畫面大小才能真正分配記憶體
	//緩衝區分配記憶體
	uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodeCtx->width, pCodeCtx->height));
	//初始化緩衝區
	avpicture_fill((AVPicture *)yuvFrame, out_buffer, AV_PIX_FMT_YUV420P, pCodeCtx->width, pCodeCtx->height);


	//輸出檔案
	FILE* fp_yuv = fopen(output_cstr,"wb");

	//用於畫素格式轉換或者縮放
	struct SwsContext *sws_ctx = sws_getContext(
			pCodeCtx->width, pCodeCtx->height, pCodeCtx->pix_fmt,
			pCodeCtx->width, pCodeCtx->height, AV_PIX_FMT_YUV420P,
			SWS_BILINEAR, NULL, NULL, NULL);

	int len ,got_frame, framecount = 0;
	//6.一陣一陣讀取壓縮的視訊資料AVPacket
	while(av_read_frame(pFormatCtx,packet) >= 0){
		//判斷是不是videoStream
		if(packet.stream_index == video_stream_idx){
			//解碼AVPacket->AVFrame
			len = avcodec_decode_video2(pCodeCtx, frame, &got_frame, packet);
			//Zero if no frame could be decompressed
			//非零,正在解碼
			if(got_frame){
				//frame->yuvFrame (YUV420P)
				//轉為指定的YUV420P畫素幀
				sws_scale(sws_ctx,
						frame->data,frame->linesize, 0, frame->height,
						yuvFrame->data, yuvFrame->linesize);

				//向YUV檔案儲存解碼之後的幀資料
				//AVFrame->YUV
				//一個畫素包含一個Y
				int y_size = pCodeCtx->width * pCodeCtx->height;
				fwrite(yuvFrame->data[0], 1, y_size, fp_yuv);
				fwrite(yuvFrame->data[1], 1, y_size/4, fp_yuv);
				fwrite(yuvFrame->data[2], 1, y_size/4, fp_yuv);

				LOGI("解碼%d幀",framecount++);
			}

			av_free_packet(packet);
		}
	}

	fclose(fp_yuv);

	av_frame_free(&frame);
	avcodec_close(pCodeCtx);
	avformat_free_context(pFormatCtx);

	(*env)->ReleaseStringUTFChars(env,input_jstr,input_cstr);
	(*env)->ReleaseStringUTFChars(env,output_jstr,output_cstr);

}

複製程式碼

上面函式就是實現視訊解碼的核心程式碼,這裡將重點分析視訊解碼的步驟。

1)初始化庫(註冊元件)

注意:這裡需要引入

//編碼
#include "include/libavcodec/avcodec.h"
//封裝格式處理
#include "include/libavformat/avformat.h"
複製程式碼

使用av_register_all()來註冊所有檔案和編解碼庫,所以他們被自動的使用在被開啟的合適格式的檔案上,av_register_all()只需要呼叫一次即可,一般我們在主函式中呼叫。

接著拿到封裝格式上下文:

AVFormatContext *pFormatCtx = avformat_alloc_context();
複製程式碼

2)開啟輸入視訊檔案

if(avformat_open_input(&pFormatCtx,input_cstr,NULL,NULL) != 0){
	LOGE("%s","開啟輸入視訊檔案失敗");
	return;
}
複製程式碼

通過第一個引數來獲得檔名。這個函式讀取檔案的頭部並且把資訊儲存到AVFormatContext 結構體中,最後三個引數用來指定特殊的檔案格式,緩衝大小和格式引數,但如果把它們設定為NULL或者0,libavformat將自動檢測這些引數。

3)獲取視訊資訊

if(avformat_find_stream_info(pFormatCtx,NULL) < 0){
	LOGE("%s","獲取視訊資訊失敗");
	return;
}
複製程式碼

4)找到視訊對應的AVStream所在pFormatCtx->streams的索引位置

int video_stream_idx = -1;
int i = 0;
for(; i < pFormatCtx->nb_streams;i++){
	//根據型別判斷,是否是視訊流
	if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
		video_stream_idx = i;
		break;
	}
}
複製程式碼

5)獲取視訊解碼器

AVCodecContext *pCodeCtx = pFormatCtx->streams[video_stream_idx]->codec;
複製程式碼

每個流是由不同的編碼 器來編碼生成的。編解碼器描述了實際的資料是如何被編碼 Coded 和解碼 DECoded 的,因此它的名字叫做 CODEC。

流中關於編解碼器的資訊就是被我們叫做"AVCodecContext "(編解碼器上下文)的東西。這裡麵包含了流中所使用的關於編解碼器的所有資訊,現在我們有了一個指向他的指標。但是我們必需要找到真正的編解碼器並且開啟它:

AVCodec *pCodec = avcodec_find_decoder(pCodeCtx->codec_id);
if(pCodec == NULL){
	LOGE("%s","無法解碼");
	return;
}

//開啟解碼器
if(avcodec_open2(pCodeCtx,pCodec,NULL) < 0){
	LOGE("%s","解碼器無法開啟");
	return;
}
複製程式碼

6)儲存資料

需要儲存幀:

//編碼資料
AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));
//畫素資料(解碼資料)
AVFrame *frame = av_frame_alloc();
複製程式碼

這裡我們需要知道一個概念,什麼是幀? Frame:音訊流和視訊流中的資料元素被稱為幀

把原始的幀轉換成一個特定的格式。讓我們先為轉換來申請一幀的記憶體:

AVFrame *yuvFrame = av_frame_alloc();
複製程式碼

即使申請了一幀的記憶體,當轉換的時候,仍然需要一個地方來存放原始的資料。

//只有指定了AVFrame的畫素格式、畫面大小才能真正分配記憶體
//緩衝區分配記憶體
uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodeCtx->width, pCodeCtx->height));
//初始化緩衝區
avpicture_fill((AVPicture *)yuvFrame, out_buffer, AV_PIX_FMT_YUV420P, pCodeCtx->width, pCodeCtx->height);
複製程式碼

av_malloc:簡單的 malloc的包裝,這樣來保證記憶體地址是對齊的(4 位元組對齊或者 2 位元組對齊)。但並不能保護你不被記憶體洩漏,重複釋放或者其它malloc的問題所困擾。

avpicture_fill:把幀和新申請的記憶體來結合。

AVPicture結構: AVPicture結構體是 AVFrame結構體的子集――AVFrame 結構體的開始部分與 AVPicture 結構體是一樣的。

7)讀取資料

int len ,got_frame, framecount = 0;
//一幀一幀讀取壓縮的視訊資料AVPacket
while(av_read_frame(pFormatCtx,packet) >= 0){
	//判斷是不是videoStream
	if(packet.stream_index == video_stream_idx){
		//解碼AVPacket->AVFrame
		len = avcodec_decode_video2(pCodeCtx, frame, &got_frame, packet);
		//Zero if no frame could be decompressed
		//非零,正在解碼
		if(got_frame){
			//frame->yuvFrame (YUV420P)
			//轉為指定的YUV420P畫素幀
			sws_scale(sws_ctx,
					frame->data,frame->linesize, 0, frame->height,
					yuvFrame->data, yuvFrame->linesize);

			//向YUV檔案儲存解碼之後的幀資料
			//AVFrame->YUV
			//一個畫素包含一個Y
			int y_size = pCodeCtx->width * pCodeCtx->height;
			fwrite(yuvFrame->data[0], 1, y_size, fp_yuv);
			fwrite(yuvFrame->data[1], 1, y_size/4, fp_yuv);
			fwrite(yuvFrame->data[2], 1, y_size/4, fp_yuv);

			LOGI("解碼%d幀",framecount++);
		}

		av_free_packet(packet);
	}
}
複製程式碼

迴圈過程:

av_read_frame():讀取一個包並且把它儲存到AVPacket結構體中。注意我們僅僅申請了一個包的結構體,ffmpeg為我們申請了內部的資料的記憶體並通過packet.data指標來指向它。這些資料可以在後面通過av_free_packet()來釋放。

avcodec_decode_video():把包轉換為幀。然而當解碼一個包的時候,我們可能沒有得到我們需要的關於幀的資訊。因此,當我們得到下一幀的時候,avcodec_decode_video()為我們設定了幀結束標誌frameFinished

8)釋放記憶體

fclose(fp_yuv);

av_frame_free(&frame);
avcodec_close(pCodeCtx);
avformat_free_context(pFormatCtx);

(*env)->ReleaseStringUTFChars(env,input_jstr,input_cstr);
(*env)->ReleaseStringUTFChars(env,output_jstr,output_cstr);
複製程式碼

使用 av_free 來釋放我們使用av_frame_allocav_malloc來分配的記憶體。

7、呼叫

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}
	public void decode(View btn){
		String input = new File(Environment.getExternalStorageDirectory()+"/mnt/shared/Other/","input.mp4").getAbsolutePath();
		String output = new File(Environment.getExternalStorageDirectory()+"/mnt/shared/Other/","output_1280x720_yuv420p.yuv").getAbsolutePath();
		VideoUtils.decode(input, output);
	}
}
複製程式碼

以上就是使用FFmpeg實現視訊解碼的整個過程,不同的專案需求不一樣,但是解碼流程基本一致,你可以在讀取資料時做一些處理,符合自己的專案需求。

相關文章