現在、ライブ ブロードキャスト プロジェクトを含む多くのビデオ プロジェクトではビデオ デコードに FFmpeg が使用されているため、ビデオ デコードは FFmpeg を学習する重要なポイントの 1 つです。
前回の記事「クラウドサーバーUbuntu上にNDK環境を構築してFFmpegをコンパイルする」でFFmpegのコンパイルを行いました。この記事では、コンパイルされた so ライブラリを使用してビデオ デコードを実装します。
この実践プロジェクトは、ローカルのビデオ ファイルをデコードし、デコード後にローカルに保存する機能を実現します。
1.DNKプロジェクトを構築する
以前紹介したNDKプロジェクトをビルドします。Eclipseでビルドする場合は「EclipseでAndroid NDK開発環境を構築する」を参照してください。AndroidStudioでビルドする場合は「」を参照してください。 Android Studio は ndk 開発プロセスをビルドします。この例では、Eclipse を使用してビルドします。
2. コンパイルされた so ライブラリとインクルード ファイルを JNI ディレクトリにコピーします。
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");
}
}
上の図では、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 ファイルを作成します
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()
一度呼び出すだけで済みます。通常、main 関数で呼び出します。
次に、カプセル化形式のコンテキストを取得します。
AVFormatContext *pFormatCtx = avformat_alloc_context();
2) 入力ビデオファイルを開きます
if(avformat_open_input(&pFormatCtx,input_cstr,NULL,NULL) != 0){
LOGE("%s","打开输入视频文件失败");
return;
}
最初のパラメータからファイル名を取得します。この関数は、ファイルのヘッダーを読み取り、その情報を AVFormatContext 構造に保存します。最後の 3 つのパラメーターは、特別なファイル形式、バッファー サイズ、および形式パラメーターを指定するために使用されますが、これらが 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();
ここで、フレームとは何なのかという概念を知る必要があります。フレーム: オーディオ ストリームとビデオ ストリームのデータ要素はフレームと呼ばれます。
生のフレームを特定の形式に変換します。まず、トランジション用に 1 フレームのメモリを割り当てましょう。
AVFrame *yuvFrame = av_frame_alloc();
1フレーム分のメモリを申請した場合でも、変換時に元のデータを保存する場所が必要です。
//只有指定了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_alloc
av_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 を使用してビデオ デコードを実現するプロセス全体です。プロジェクトによって要件は異なりますが、デコード プロセスは基本的に同じです。プロジェクトの要件に合わせて、データを読み込むときに何らかの処理を行うことができます。
オリジナル NDK 開発 - FFmpeg ビデオ デコード - Nuggets
★記事末尾の名刺では、オーディオ・ビデオ開発学習教材(FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、srs)やオーディオ・ビデオ学習ロードマップ等を無料で受け取ることができます。
下記参照!