前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >偶遇FFmpeg(番外)——FFmpeg花样编译入魔1之裁剪大小

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

作者头像
deep_sadness
发布2018-10-25 16:30:03
3.1K0
发布2018-10-25 16:30:03
举报
文章被收录于专栏:Flutter入门Flutter入门

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

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

例子实战

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

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

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

回顾FFmpeg configure

  • 首先,回顾一下前文中的内容。 编译时,我们可以针对自己需要的功能来进行配置,更改bash脚本。选择需要编译的部分,进行编译。就能缩小大小。 整体的配置部分如下
代码语言:javascript
复制
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图像,可以无损提取。
代码语言:javascript
复制
ffmpeg -i .../some_mjpeg.avi -c:v frames_%d.jpg

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

代码语言:javascript
复制
ffmpeg -i mpeg-movie.avi -c:v copy -bsf:v mjpeg2jpeg frames_%d.jpg

类似这种对流的处理的。

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

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

filters过滤器

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

确定需求并编写脚本

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

1. 通过FFmpeg -i来得到视频的完整信息
代码语言:javascript
复制
ffmpeg -i video.mp4

视频信息.png

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

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

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

  1. 先关闭所有的模块
代码语言:javascript
复制
--disable-everything 
  1. 打开需要的模块
代码语言:javascript
复制
    --enable-decoder=h264 \
    --enable-decoder=aac \
    --enable-parser=aac \
    --enable-parser=h264 \
    --enable-demuxer=mp4 \
    --enable-protocol=file \
编译结果
测试代码

ffmpeg_player.c

代码语言:javascript
复制
#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...我们当时似乎是忽略了这几个。那添加上看看。

  • 在脚本上添加
代码语言:javascript
复制
--enable-demuxer=mov \
--enable-demuxer=m4a \
编译后的大小
  • 最后的脚本
代码语言:javascript
复制
#!/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命令参数 [总结]视音频编解码技术零基础学习方法

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.10.18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目标确定- 不择手段得最小
  • 例子实战
    • 回顾FFmpeg configure
      • 各部分意思
    • 确定需求并编写脚本
      • 1. 通过FFmpeg -i来得到视频的完整信息
      • 2.编写编译脚本
      • 编译结果
      • 测试代码
      • 编译后的大小
    • 遇到问题!!!
      • 编译后的大小
      • 测试通过!!
      • 1. 裁剪的方法
      • 2. 遇到问题的解决方案
  • 总结
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档