美文网首页
偶遇FFmpeg(番外)——FFmpeg花样编译入魔1之裁剪大小

偶遇FFmpeg(番外)——FFmpeg花样编译入魔1之裁剪大小

作者: deep_sadness | 来源:发表于2018-10-18 10:28 被阅读93次

    目标确定- 不择手段得最小

    偶遇FFmpeg(三)——Android集成这边文章中曾经介绍过FFmpeg和Android的交叉编译。文章中也提到过如何裁剪SO文件大小的方式。
    这边文章就这个问题。进行实战。

    例子实战

    下面将会用这个需求的例子来说明,如果裁剪SO文件的大小。

    • 需求
      读取手机上的视频文件,将其转换成yuv,进行保存。

    因为我们要求编译的最小,所以我们需要让我们的FFmpeg编译的结果,只要满足这个功能就足够。其他的都不需要。

    回顾FFmpeg configure

    • 首先,回顾一下前文中的内容。
      编译时,我们可以针对自己需要的功能来进行配置,更改bash脚本。选择需要编译的部分,进行编译。就能缩小大小。
      整体的配置部分如下
    Individual component options:
    --disable-everything     disable all components listed below
    --disable-encoder=NAME   disable encoder NAME
    --enable-encoder=NAME   enable encoder NAME
    --disable-encoders       disable all encoders
    --disable-decoder=NAME   disable decoder NAME
    --enable-decoder=NAME   enable decoder NAME
    --disable-decoders       disable all decoders
    --disable-hwaccel=NAME   disable hwaccel NAME
    --enable-hwaccel=NAME   enable hwaccel NAME
    --disable-hwaccels       disable all hwaccels
    --disable-muxer=NAME     disable muxer NAME
    --enable-muxer=NAME     enable muxer NAME
    --disable-muxers         disable all muxers
    --disable-demuxer=NAME   disable demuxer NAME
    --enable-demuxer=NAME   enable demuxer NAME
    --disable-demuxers      disable all demuxers
    --enable-parser=NAME     enable parser NAME
    --disable-parser=NAME   disable parser NAME
    --disable-parsers       disable all parsers
    --enable-bsf=NAME       enable bitstream filter NAME
    --disable-bsf=NAME       disable bitstream filter NAME
    --disable-bsfs           disable all bitstream filters
    --enable-protocol=NAME   enable protocol NAME
    --disable-protocol=NAME disable protocol NAME
    --disable-protocols     disable all protocols
    --enable-indev=NAME     enable input device NAME
    --disable-indev=NAME     disable input device NAME
    --disable-indevs         disable input devices
    --enable-outdev=NAME     enable output device NAME
    --disable-outdev=NAME   disable output device NAME
    --disable-outdevs       disable output devices
    --disable-devices       disable all devices
    --enable-filter=NAME     enable filter NAME
    --disable-filter=NAME   disable filter NAME
    --disable-filters       disable all filters
    

    各部分意思

    下面对照两个流程来理解一下各个部分的作用。
    理解下面的流程,对后续裁剪过程中,遇到问题时,查找问题十分关键。

    播放的流程

    结合这张图播放的流程,我们理解这各部分。


    播放流程.png
    1. 输入数据开始,需要进行解协议。这个协议的部分就是protocol来负责的。
    2. 解封装。解封装需要的就是demuxers。同样,对于一个文件,只有找到对应的解封装器,才能成功。
    3. 就开始分别对音频和视频文件进行解码。
      解码需要两个部分。
      一个是解析器parser
      用于解析码流的AVCodecParser结构体。用于解析HEVC码流中的一些信息(例如SPS、PPS、Slice Header等)
      一个是解码器decoder
      用于解码码流的AVCodec结构体。通过帧内预测、帧间预测等方法解码CTU压缩数据。
      接下来,就要交给对应的设备进行播放了。
    录制的流程

    相对的录制的流程,
    就是和上面相反,

    1. 输入原始的数据,通过编码器encoder进行编码
    2. 再通过封装器muxer进行封装。
    3. 在通过协议protocol,进行传输
    流程中未说明的部分:
    hwaccels硬件加速器

    对应平台的硬件加速的编解码器。可用通过使用对应平台有的解码器,进行硬件加速。

    bsfs应用于bit流的过滤器

    应用于流的过滤器。通常是因为流中的信息,转换成其他形式而缺少。就可以通过这个滤镜进行补充进行,然后转换。

    • 比如将mpeg.avi 截图成 jpeg.
      因为MJPEG是一种视频编码,它的每一帧基本上是一个JPEG图像,可以无损提取。
    ffmpeg -i .../some_mjpeg.avi -c:v frames_%d.jpg
    

    但是它却不是完整的图像,还缺少必要的DHT段。
    所以需要使用bit流过滤器,修复MJPEG流为完成的JPEG图像,就可以得到每一帧的图像了。

    ffmpeg -i mpeg-movie.avi -c:v copy -bsf:v mjpeg2jpeg frames_%d.jpg
    

    类似这种对流的处理的。

    indevs可用的输入设备和outdevs可用的输出设备

    整个基本上在Android上不会用到

    filters过滤器

    可用于文件的过滤器,如宽高比裁剪,格式化、非格式化 伸缩等。
    通常我们需要对音频进行缩放,所以我们还是需要他的。

    确定需求并编写脚本

    知道各个模块部分的作用之后,我们需要确定,我们需要的模块。因为我们只是想播放一个视频。所以我们直接可以根据这个视频的信息来选择,我们需要的部分。

    1. 通过FFmpeg -i来得到视频的完整信息

    ffmpeg -i video.mp4
    
    视频信息.png

    因为我们只是播放视频,所以我们只需要播放流程中的protocoldemuxerdecoderparser
    从上图信息,我们可以知道

    • decoder 和 parser
      我们需要的视频的decoderh264,音频的decoderaac。同时,我们回顾到parser通常和decoder是成对出现的。那同样为parser添加h264aac
    • demuxer
      因为我们的视频是mp4的,所以我们使用mp4
    • protocol
      最后,因为我们是需要播放本地的文件。所以需要添加file协议

    2.编写编译脚本

    在原来的编译脚本上,添加上我们的裁剪的脚本

    1. 先关闭所有的模块
    --disable-everything 
    
    1. 打开需要的模块
        --enable-decoder=h264 \
        --enable-decoder=aac \
        --enable-parser=aac \
        --enable-parser=h264 \
        --enable-demuxer=mp4 \
        --enable-protocol=file \
    

    编译结果

    测试代码

    ffmpeg_player.c

    #include <jni.h>
    #include <libavformat/avformat.h>
    
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libswscale/swscale.h>
    #include <libavfilter/avfilter.h>
    
    #include "android/log.h"
    
    
    #define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jason",FORMAT,##__VA_ARGS__);
    #define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jason",FORMAT,##__VA_ARGS__);
    
    JNIEXPORT void JNICALL
    Java_com_ffmpeg_VideoUtils_decode(JNIEnv *env, jclass type, jstring input_, jstring output_) {
        const char *input_cstr = (*env)->GetStringUTFChars(env, input_, 0);
        const char *output_cstr = (*env)->GetStringUTFChars(env, output_, 0);
        //    //需要转码的视频文件(输入的视频文件)
    
        //1.注册所有主键
        av_register_all();
        //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
        AVFormatContext *avFormatContext = avformat_alloc_context();
    
        //2.打开输入视频文件夹
        int err_code = avformat_open_input(&avFormatContext, input_cstr, NULL, NULL);
        if (err_code != 0) {
            char errbuf[1024];
            const char *errbuf_ptr = errbuf;
            av_strerror(err_code, errbuf_ptr, sizeof(errbuf));
            LOGE("Couldn't open file %s: %d(%s)", input_cstr, err_code, errbuf_ptr);
            LOGE("%s", "打开输入视频文件失败");
            return;
        }
    
        //3.获取视频文件信息
        avformat_find_stream_info(avFormatContext, NULL);
    
        //获取视频流的索引位置
        //遍历所有类型的流(音频流、视频流、字幕流),找到视频流
        int v_stream_idx = -1;
        int i = 0;
        for (; i < avFormatContext->nb_streams; i++) {
            if (avFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                v_stream_idx = i;
                break;
            }
        }
    
        if (v_stream_idx == -1) {
            LOGE("%s", "找不到视频流\n");
            return;
        }
    
        //只有知道视频的编码方式,才能够根据编码方式去找到解码器
        //获取视频流中的编解码上下文
        AVCodecContext *pCodecCtx = avFormatContext->streams[v_stream_idx]->codec;
        //4.根据编解码上下文中的编码id查找对应的解码
        AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
        if (pCodec == NULL) {
            LOGE("%s", "找不到解码器\n");
            return;
        }
    
    
        //5.打开解码器
        if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
            LOGE("%s", "解码器无法打开\n");
            return;
        };
    
        //准备读取
        //AVPacket用于存储一帧一帧的压缩数据(H264)
        //缓冲区,开辟空间
        AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
    
        //AVFrame用于存储解码后的像素数据(YUV)
        //内存分配
        AVFrame *pFrame = av_frame_alloc();
        //YUV420
        AVFrame *pFrameYUV = av_frame_alloc();
    
        //只有指定了AVFrame的像素格式、画面大小才能真正分配内存
        //缓冲区分配内存
        uint8_t *out_buffer = (uint8_t *) av_malloc(
                avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
        //初始化缓冲区
        avpicture_fill((AVPicture *) pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width,
                       pCodecCtx->height);
    
    
    //    //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等
    //    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
    //                                                pCodecCtx->pix_fmt,
    //                                                pCodecCtx->width, pCodecCtx->height,
    //                                                AV_PIX_FMT_YUV420P,
    //                                                SWS_BICUBIC, NULL, NULL, NULL);
    
    
        int got_picture, ret;
    
        FILE *fp_yuv = fopen(output_cstr, "wb+");
    
        int frame_count = 0;
    
    //    6.一帧一帧的读取压缩数据
        int readCode = av_read_frame(avFormatContext, packet);
        LOGI("av_read_frame error = %d", readCode);
        while ( readCode>= 0) {
    
            //只要视频压缩数据(根据流的索引位置判断)
            if (packet->stream_index == v_stream_idx) {
                //7.解码一帧视频压缩数据,得到视频像素数据
                ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
                if (ret < 0) {
                    LOGE("%s", "解码错误");
                    return;
                }
    
                //为0说明解码完成,非0正在解码
                if (got_picture) {
                    //AVFrame转为像素格式YUV420,宽高
                    //2 6输入、输出数据
                    //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的
                    //4 输入数据第一列要转码的位置 从0开始
                    //5 输入画面的高度
    //                sws_scale(sws_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
    //                          pFrameYUV->data, pFrameYUV->linesize);
    
                    //输出到YUV文件
                    //AVFrame像素帧写入文件
                    //data解码后的图像像素数据(音频采样数据)
                    //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感
                    //U V 个数是Y的1/4
                    int y_size = pCodecCtx->width * pCodecCtx->height;
                    fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                    fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                    fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);
    
                    frame_count++;
                    LOGI("解码第%d帧", frame_count);
                }
            }
    
            //释放资源
            av_free_packet(packet);
            readCode = av_read_frame(avFormatContext, packet);
            LOGI("av_read_frame error = %d", readCode);
        }
    
        fclose(fp_yuv);
    
        (*env)->ReleaseStringUTFChars(env, input_, input_cstr);
        (*env)->ReleaseStringUTFChars(env, output_, output_cstr);
    
        av_frame_free(&pFrame);
    
        avcodec_close(pCodecCtx);
    
        avformat_free_context(avFormatContext);
    
    }
    
    
    JNIEXPORT jstring JNICALL
    Java_com_ffmpeg_VideoUtils_avFormatInfo(
            JNIEnv *env,
            jobject jobject1/* this */) {
        char info[40000] = {0};
        av_register_all();
        AVInputFormat *if_temp = av_iformat_next(NULL);
        AVOutputFormat *of_temp = av_oformat_next(NULL);
        while (if_temp != NULL) {
            sprintf(info, "fromCppLog   %sInput: %s\n", info, if_temp->name);
            if_temp = if_temp->next;
        }
        while (of_temp != NULL) {
            sprintf(info, "fromCppLog   %sOutput: %s\n", info, of_temp->name);
            of_temp = of_temp->next;
        }
        return (*env)->NewStringUTF(env, info);
    }
    
    JNIEXPORT jstring JNICALL
    Java_com_ffmpeg_VideoUtils_urlProtocolInfo(
            JNIEnv *env,
            jobject jobject1 /* this */) {
        char info[40000] = {0};
        av_register_all();
        struct URLProtocol *pup = NULL;
        struct URLProtocol **p_temp = &pup;
        avio_enum_protocols((void **) p_temp, 0);
        while ((*p_temp) != NULL) {
            sprintf(info, "%sInput: %s\n", info, avio_enum_protocols((void **) p_temp, 0));
        }
        pup = NULL;
        avio_enum_protocols((void **) p_temp, 1);
        while ((*p_temp) != NULL) {
            sprintf(info, "%sInput: %s\n", info, avio_enum_protocols((void **) p_temp, 1));
        }
        return (*env)->NewStringUTF(env, info);
    }
    
    JNIEXPORT jstring JNICALL
    Java_com_ffmpeg_VideoUtils_avCodecInfo(
            JNIEnv *env,
            jobject /* this */oj) {
        char info[40000] = {0};
        av_register_all();
        AVCodec *c_temp = av_codec_next(NULL);
        while (c_temp != NULL) {
            if (c_temp->decode != NULL) {
                sprintf(info, "%sdecode:", info);
            } else {
                sprintf(info, "%sencode:", info);
            }
            switch (c_temp->type) {
                case AVMEDIA_TYPE_VIDEO:
                    sprintf(info, "%s(video):", info);
                    break;
                case AVMEDIA_TYPE_AUDIO:
                    sprintf(info, "%s(audio):", info);
                    break;
                default:
                    sprintf(info, "%s(other):", info);
                    break;
            }
            sprintf(info, "%s[%10s]\n", info, c_temp->name);
            c_temp = c_temp->next;
        }
        return (*env)->NewStringUTF(env, info);
    }
    
    JNIEXPORT jstring JNICALL
    Java_com_ffmpeg_VideoUtils_avFilterInfo(JNIEnv *env, jobject /* this */oj) {
        char info[40000] = {0};
        avfilter_register_all();
        AVFilter *f_temp = (AVFilter *) avfilter_next(NULL);
        while (f_temp != NULL) {
            sprintf(info, "%s%s\n", info, f_temp->name);
            f_temp = f_temp->next;
        }
        return (*env)->NewStringUTF(env, info);
    }
    
    • Java_com_ffmpeg_VideoUtils_decode 方法
      这就是我们的目标代码,输入mp4文件,将其解码为yuv,并保存下来。
      观察代码,就会发现上面提到的播放流程。

    • 其他方法
      其他方法就是帮助我们调试的方法,能够得到当前编译的库内的这些模块的情况

    编译后的大小

    编译结果1.png

    Great!!!看起来很不错。压缩之后,才800多K。
    那我们来测试一下吧~

    遇到问题!!!

    晴天霹雳.png

    打开输入文件失败!!!
    宛如晴天霹雳。难道我们自己预设的裁剪方法错误了?

    定位问题

    重新回到上面分析的方法,回顾整体的流程。
    打开视频文件失败,应该是解封装这步出现了问题。
    如果是上一步,则会提示协议错误。下一步,应该是解码错误。


    回顾流程.png
    查找解决

    在确定问题后,我们再次去看看视频的信息情况。


    确定问题.png

    em...我们当时似乎是忽略了这几个。那添加上看看。

    • 在脚本上添加
    --enable-demuxer=mov \
    --enable-demuxer=m4a \
    

    编译后的大小

    • 最后的脚本
    #!/bin/bash
    
    NDK=/Users/Cry/Library/Android/sdk/android-ndk-r14b
    SYSROOT=$NDK/platforms/android-14/arch-arm/
    TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
    
    CPU=arm
    # PREFIX=$(pwd)/android/$CPU
    PREFIX=/Users/Cry/Documents/FFmpeg/1017/small_test2/$CPU
    ADDI_CFLAGS=""
    ADDI_LDFLAGS=""
    
    function build_arm
    {
    ./configure \
        --prefix=$PREFIX \
        --enable-shared \
        --disable-everything \
        --enable-decoder=h264 \
        --enable-decoder=aac \
        --enable-parser=aac \
        --enable-parser=h264 \
        --enable-demuxer=mp4 \
        --enable-demuxer=mov \
        --enable-demuxer=m4a \
        --enable-protocol=file \
        --enable-filter=scale \
        --disable-static \
        --disable-doc \
        --disable-ffmpeg \
        --disable-ffplay \
        --disable-ffprobe \
        --disable-ffserver \
        --disable-symver \
        --disable-avresample \
        --enable-small \
        --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
        --target-os=linux \
        --arch=arm \
        --enable-cross-compile \
        --sysroot=$SYSROOT \
        --extra-cflags="-Os -fpic $ADDI_CFLAGS" \
        --extra-ldflags="$ADDI_LDFLAGS" \
        $ADDITIONAL_CONFIGURE_FLAG
    make clean
    make
    make install
    }
    
    build_arm
    
    • 结果大小


      最后结果.png

    测试通过!!

    • avFormatInfo 方法
      image.png
    • 运行log


      运行.png
    • APK中的大小


      APK.png

    撒花~~~

    总结

    本文就是通过一个实际的例子,来说明如何裁剪FFmpeg编译大小的解决思路。

    1. 裁剪的方法

    我们可以通过configure中定义的编译参数,来定制我们需要的模块。

    2. 遇到问题的解决方案

    而定制模块时,需要时刻牢记代码执行的流程。

    • 如果是播放的话,则是


      image.png

    当遇到问题时,按图索骥,找到对应的问题发生的点,然后再去查找是不是有所遗漏,来解决问题。


    参考

    ffmpeg configure命令参数
    [总结]视音频编解码技术零基础学习方法

    相关文章

      网友评论

          本文标题:偶遇FFmpeg(番外)——FFmpeg花样编译入魔1之裁剪大小

          本文链接:https://www.haomeiwen.com/subject/qdtdzftx.html