NDK学习笔记:FFmpeg解压MP4提取视频YUV

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a360940265a/article/details/84865793

NDK学习笔记:FFmpeg解压MP4提取视频YUV

继上一篇NDK的开发笔记,既然我们已经从源码手动编译ffmpeg-so出来了,这篇文章就当是检验编译的so是否可用,对FFmpeg进行一番学习,写一个最简单的例子。并结合工作中的一些架构内容,推出一些简单架构的话题。欢迎大家互相学习。事不宜迟,马上撸码。

一、准备工作

定义native方法的java入口。FFmpegUtils

public class ZzrFFmpeg {


    public static native int Mp4TOYuv(String input_path_str, String output_path_str );

    static
    {
        try {
            System.loadLibrary("avutil");
            System.loadLibrary("swscale");
            System.loadLibrary("swresample");
            System.loadLibrary("avcodec");
            System.loadLibrary("avformat");
            System.loadLibrary("postproc");
            System.loadLibrary("avfilter");
            System.loadLibrary("avdevice");
            //以上库最后要手动放置到jniLibs文件夹的对应ANDROID_ABI当中
            //System.loadLibrary("zzr-ffmpeg-utils"); 
            //我们自己编写方法的库,最后生成之后要打开注释
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里要强调一点,以上的so加载顺序是有序的!其中依赖关系我们可以查阅ffmpeg的编译配置configure(关于编译与配置有不明白的,请参考上一篇NDK的文章)依赖关系如下所示:

# libraries, in linking order
avcodec_deps="avutil"
avdevice_deps="avformat avcodec avutil"
avfilter_deps="avutil"
avformat_deps="avcodec avutil"
avresample_deps="avutil"
postproc_deps="avutil gpl"
swresample_deps="avutil"
swscale_deps="avutil"

建立好native入口的java文件之后,我们就可以开始实现native方法了。我们在项目的cpp中新建文件夹ffmpeg,把我们手动编译的ffmpeg产物全部拷贝,并新建ffmpeg_util.c文件,目录结构大概如下:

 其中我们在zzr_ffmpeg_util.c文件编写我们的native方法Mp4TOYuv的实现:

#include <jni.h>

JNIEXPORT jint JNICALL
Java_org_zzrblog_mp_ZzrFFmpeg_Mp4TOYuv(JNIEnv *env, jclass clazz, 
                                    jstring input_path_jstr, jstring output_path_jstr) 
{
    // ... ...
}

有强迫症的同学(例如我自己)可能懊恼为啥java文件的native方法还是标红报错,找不到对应的c/c++实现,rebuild还是不行。这是因为CMake编译脚本还没真正的生成符号表,所以导致找不到c/c++实现的入口。所以我们还是把CMake编译脚本准备好

# 引入外部 ffmpeg so 供源文件编译
add_library(avcodec SHARED IMPORTED )
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libavcodec.so)
set_target_properties(avcodec PROPERTIES LINKER_LANGUAGE CXX)

add_library(avdevice SHARED IMPORTED )
set_target_properties(avdevice PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libavdevice.so)
set_target_properties(avdevice PROPERTIES LINKER_LANGUAGE CXX)

add_library(avfilter SHARED IMPORTED )
set_target_properties(avfilter PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libavfilter.so)
set_target_properties(avfilter PROPERTIES LINKER_LANGUAGE CXX)

add_library(avformat SHARED IMPORTED )
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libavformat.so)
set_target_properties(avformat PROPERTIES LINKER_LANGUAGE CXX)

add_library(avutil SHARED IMPORTED )
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libavutil.so)
set_target_properties(avutil PROPERTIES LINKER_LANGUAGE CXX)

add_library(postproc SHARED IMPORTED )
set_target_properties(postproc PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libpostproc.so)
set_target_properties(postproc PROPERTIES LINKER_LANGUAGE CXX)

add_library(swresample SHARED IMPORTED )
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libswresample.so)
set_target_properties(swresample PROPERTIES LINKER_LANGUAGE CXX)

add_library(swscale SHARED IMPORTED )
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/ffmpeg/lib/libswscale.so)
set_target_properties(swscale PROPERTIES LINKER_LANGUAGE CXX)

add_library( # 生成动态库的名称
             zzr-ffmpeg-utils
             # 指定是动态库SO
             SHARED
             # 编译库的源代码文件
             src/main/cpp/ffmpeg/zzr_ffmpeg_util.c)

target_link_libraries( # 指定目标链接库
                       zzr-ffmpeg-utils
                       # 添加预编译库到目标链接库中
                       ${log-lib}
                       avutil
                       avcodec
                       avformat
                       swscale )

我们在之前的fmod基础上,很快就可以理解并编写脚本。如果有问题的同学可以去查看之前的fmod(下)文章

二、实现Mp4TOYuv (RGB)(H264)

到这里我们开始编写一个ffmpeg最简单的解码例子。并从中学习ffmpeg的使用步骤。在写代码之前,我们先看看 “雷神” 雷霄骅的一页ppt教学。缅怀念这位同期的伟人。愿天堂还能继续做自己喜欢的研究 R.I.P

 这页ppt对应的是3.x之前的版本的API,我们使用的是3.3.9略有变化,但是大体流程是一致的。用雷神的原话:初次学习,一定要将这流程和函数名称熟记于心。  现在开始真正的编码。

JNIEXPORT jint JNICALL
Java_org_zzrblog_mp_ZzrFFmpeg_Mp4TOYuv(JNIEnv *env, jclass clazz, jstring input_path_jstr, jstring output_path_jstr) {

    const char *input_path_cstr = (*env)->GetStringUTFChars(env, input_path_jstr, 0);
    const char *output_path_cstr = (*env)->GetStringUTFChars(env, output_path_jstr, 0);
    LOGD("输入文件:%s", input_path_cstr);
    LOGD("输出文件:%s", output_path_cstr);

    // 1.注册组件
    av_register_all();
    avcodec_register_all();
    avformat_network_init();
    // 2.获取格式上下文指针,便于打开媒体容器文件获取媒体信息
    AVFormatContext *pFormatContext = avformat_alloc_context();
    // 打开输入视频文件
    if(avformat_open_input(&pFormatContext, input_path_cstr, NULL, NULL) != 0){
        LOGE("%s","打开输入视频文件失败");
        return -1;
    }
    // 获取视频信息
    if(avformat_find_stream_info(pFormatContext,NULL) < 0){
        LOGE("%s","获取视频信息失败");
        return -2;
    }

    // 3.准备视频解码器,根据视频AVStream所在pFormatCtx->streams中位置,找出索引
    int video_stream_idx = -1;
    for(int i=0; i<pFormatContext->nb_streams; i++)
    {
        //根据类型判断,是否是视频流
        if(pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_idx = i;
            break;
        }
    }
    LOGD("VIDEO的索引位置:%d", video_stream_idx);
    // 根据codec_parameter的codec索引,提取对应的解码器。
    AVCodec *pCodec = avcodec_find_decoder(pFormatContext->streams[video_stream_idx]->codecpar->codec_id);
    if(pCodec == NULL) {
        LOGE("%s","解码器创建失败.");
        return -3;
    }
    // 4.创建解码器对应的上下文
    AVCodecContext * pCodecContext = avcodec_alloc_context3(pCodec);
    if(pCodecContext == NULL) {
        LOGE("%s","创建解码器对应的上下文失败.");
        return -4;
    }
    // 坑位!!!
    //pCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    //pCodecContext->width = pFormatContext->streams[video_stream_idx]->codecpar->width;
    //pCodecContext->height = pFormatContext->streams[video_stream_idx]->codecpar->height;
    //pCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
    avcodec_parameters_to_context(pCodecContext, 
                                pFormatContext->streams[video_stream_idx]->codecpar);
    // 5.打开解码器
    if(avcodec_open2(pCodecContext, pCodec, NULL) < 0){
        LOGE("%s","解码器无法打开");
        return -5;
    } else {
        LOGI("设置解码器解码格式pix_fmt:%d", pCodecContext->pix_fmt);
    }

    // ... ...

    return 0;
}

 以上代码对应的是ppt上的前五步骤。我们来逐一分析:

  • 第1步:固定步骤,注册FFmpeg所需的组件,启动初始化运行环境。关于有多个xxx_register_xxx的方法,其实可以通过各种写代码编译器的智能提示,输入register就能看到也没多少注册方法,也就3~4~5~6个而已。/doge
  • 第2步:关于任何媒体容器文件(MP4、RMVB、TS、FLV、AVI)还是各种在线传输协议(udp-rtp、rtmp)在正式解码之前,我们都必须要获取当前视频的格式信息,以便更好的创建解码器&运行时环境。  ffmpeg获取视频格式信息通过三个API完成:(1)avformat_alloc_context 创建 AVFormatContext * 格式上下文指针;(2)通过avformat_open_input 关联打开 格式上下文指针 & 媒体文件/流;(3)avformat_find_stream_info 获取媒体文件/流的格式信息。
  • 第3步:到达这一步,我们已经掌握了媒体文件/流的格式信息了。通过遍历AVFormatContext->streams的 AVStream 数组,获取AVCodecParameters 编解码器的参数列表 中的codec_type编码类型。(这里AVStream代表的是媒体文件/流中具体的某一轨信息,一个正常媒体资源可能包括:视频+音频+字幕 等等其他信息)我们这里找出视频轨的索引位置,根据索引出来的AVCodecParameters->codec_id解码器ID,通过avcodec_find_decoder提取对应的AVCodec视频解码器。(这里的引用有点多而且复杂,请认真理解)(这一步骤也是3.x前后的小区别之一,请注意)
  • 第4步:你以为有AVCodec解码器就完事了吗?NONONO,此时的AVCodec解码器还没完成初始化,不知道你想要它干啥呢!此时我们需要构建AVCodecContext指针关联AVCodec解码器。注意!这一步的内容是3.x前后的明显区别之一,之前版本的API,我们是不需要这样自己创建关联。通过AVCodecContext * pCodecContext = avcodec_alloc_context3(pCodec); 我们创建AVCodecContext指针,但是此时的AVCodecContext 还需要通过新的API接口avcodec_parameters_to_context(AVCodecContext *,  AVCodecParameters * )来完成初始化工作,从上面的注释我可以诚恳的告诉大家,别想着自己完成初始化赋值工作,因为变量实在太多了,一个错了,就会导致后面解码工作的失败。所以还是乖乖的用API吧。
  • 第5步:基本工作准备好我们就可以正式启动解码器,通过avcodec_open2(AVCodecContext * , AVCodec * , AVDictionary**); 结束FFmpeg解码工作环境的建立。

接下来,我们进入解码流程:

    // ... ... 紧接上方 ... ...
    // 解码流程,多看多理解。
    // 解压缩前的数据对象
    AVPacket *packet = av_packet_alloc();
    // 解码后数据对象
    AVFrame *frame = av_frame_alloc();
    AVFrame *yuvFrame = av_frame_alloc();

    // 为yuvFrame缓冲区分配内存,只有指定了AVFrame的像素格式、画面大小才能真正分配内存
    int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecContext->width, pCodecContext->height, 1);
    uint8_t *out_buffer = (uint8_t *)av_malloc((size_t) buffer_size);
    // 初始化yuvFrame缓冲区
    av_image_fill_arrays(yuvFrame->data, yuvFrame->linesize, out_buffer,
                         AV_PIX_FMT_YUV420P, pCodecContext->width, pCodecContext->height, 1 );
    // yuv输出文件
    FILE* fp_yuv = fopen(output_path_cstr,"wb");
    // test:264输出文件
    char save264str[100]={0};
    sprintf(save264str, "%s", "/storage/emulated/0/10s_test.h264");
    FILE* fp_264 = fopen(save264str,"wb");

    //用于像素格式转换或者缩放
    struct SwsContext *sws_ctx = sws_getContext(
            pCodecContext->width, pCodecContext->height, pCodecContext->pix_fmt,
            pCodecContext->width, pCodecContext->height, AV_PIX_FMT_YUV420P,
            SWS_BICUBIC, NULL, NULL, NULL); //SWS_BILINEAR

    int ret, frameCount = 0;
    // 5. 循环读取视频数据的分包 AVPacket
    while(av_read_frame(pFormatContext, packet) >= 0)
    {
        if(packet->stream_index == video_stream_idx)
        {
            // test:h264数据写入本地文件
            fwrite(packet->data, 1, (size_t) packet->size, fp_264);
            //AVPacket->AVFrame
            ret = avcodec_send_packet(pCodecContext, packet);
            if(ret < 0){
                LOGE("avcodec_send_packet:%d\n", ret);
                continue;
            }
            while(ret >= 0) {
                ret = avcodec_receive_frame(pCodecContext, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
                    LOGD("avcodec_receive_frame:%d\n", ret);
                    break;
                }else if (ret < 0) {
                    LOGW("avcodec_receive_frame:%d\n", AVERROR(ret));
                    goto end;  //end处进行资源释放等善后处理
                }
                if (ret >= 0)
                {   //frame->yuvFrame (调整缩放)
                    sws_scale(sws_ctx,
                              (const uint8_t* const*)frame->data, frame->linesize, 0, frame->height,
                              yuvFrame->data, yuvFrame->linesize);
                    //向YUV文件保存解码之后的帧数据
                    //AVFrame->YUV,一个像素包含一个Y
                    int y_size = frame->width * frame->height;
                    fwrite(yuvFrame->data[0], 1, (size_t) y_size, fp_yuv);
                    fwrite(yuvFrame->data[1], 1, (size_t) y_size/4, fp_yuv);
                    fwrite(yuvFrame->data[2], 1, (size_t) y_size/4, fp_yuv);
                    frameCount++ ;
                }
            }
        }
        av_packet_unref(packet);
    }
    LOGI("总共解码%d帧",frameCount++);

解码流程如上,我们快速简单分析一波,然后祭出灵魂画师(我的)画的图,帮助大家理解:

1、首先创建 解压缩数据对象 AVPacket *packet,解码后数据的对象AVFrame *frame/*yuvFrame 先别问为啥有两个AVFrame。

2、然后为yuvFrame缓冲区分配内存,通过av_image_get_buffer_size计算yuv对应大小的内存大小buffer_size,然后就是av_malloc(buffer_size),最后就是av_image_fill_arrays关联绑定 内存区 和 yuvFrame。只有指定了像素格式、画面大小AVFrame才能真正分配内存。      继续别问为啥只对yuvFrame操作,frame不需要。

3、然后就是打开本地文件句柄,准备写入相关数据流。接着我们创建yuv/rgb 转换器,这部分代码比较固定而且简单。

4、然后分析关键循环,通过av_read_frame循环读取媒体文件/流,获取裸码流数据AVPacket,判断当前是否视频数据。如果是视频数据,那此时的AVPacket装载的就是h264的源数据了。我们把数据写入本地文件。循环最后需要调用av_packet_unref来解除AVPacket对象的引用次数。

接着我们把h264数据通过avcodec_send_packet(pCodecContext, packet) 发送到解码器,判断返回值是否正常,然后我可以立刻通过avcodec_receive_frame(pCodecContext, frame) 获取解码后的数据,通过此对API,我们就完成了从裸码流AVPacket->AVFrame的解码数据的转换。但请注意,AVPacket和AVFrame的关系并不是一对一的!更多时候是N个AVPacket才对应一个AVFrame!此时AVFrame的格式就是对应的之前准备工作avcodec_parameters_to_context中AVCodecParameters的格式,也就是通过FFmpeg解读出来的pix_fmt(其实就是YUV,不信你自己debug看看)

出来AVFrame之后,我们继续通过sws_scale达到yuv/rgb的格式转换,转换成之后把数据写入本地文件。

5、好了,回到1和2的两个问题,然后我们怎么对这个流程加深理解呢?看看下图:我们以之前分析Android.MediaCodec的工作原理图,然后再结合流程分析,我们就可以明白了,为啥frame不需要独立分配内存空间,而缩放后得到我们想要的yuvFrame需要费一堆API来配合工作。   配合图解还不懂的,请不要私信我~~~

重要的内容已经结束了。但是不要忘了资源回收的收尾工作。

end:
    // 结束回收工作
    fclose(fp_yuv);
    fclose(fp_264);

    sws_freeContext(sws_ctx);
    av_free(out_buffer);
    av_frame_free(&frame);
    av_frame_free(&yuvFrame);
    avcodec_close(pCodecContext);
    avcodec_free_context(&pCodecContext);
    avformat_close_input(&pFormatContext);
    avformat_free_context(pFormatContext);

    (*env)->ReleaseStringUTFChars(env, input_path_jstr, input_path_cstr);
    (*env)->ReleaseStringUTFChars(env, output_path_jstr, output_path_cstr);
    return 0;

 三、思考问题

代码确实可以运行起来了,然后也能生成.h264 和 .yuv了,然而。。。我现在每次avcodec_send_packet就立刻avcodec_receive_frame,然后直接fwrite,那新的数据岂不是~~~旧的~~~ /皱眉/皱眉/皱眉

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/84865793