目标确定- 不择手段得最小
在偶遇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
- 输入数据开始,需要进行解协议。这个协议的部分就是
protocol
来负责的。 - 解封装。解封装需要的就是
demuxers
。同样,对于一个文件,只有找到对应的解封装器,才能成功。 - 就开始分别对音频和视频文件进行解码。
解码需要两个部分。
一个是解析器parser
。
用于解析码流的AVCodecParser结构体。用于解析HEVC码流中的一些信息(例如SPS、PPS、Slice Header等)
一个是解码器decoder
。
用于解码码流的AVCodec结构体。通过帧内预测、帧间预测等方法解码CTU压缩数据。
接下来,就要交给对应的设备进行播放了。
录制的流程
相对的录制的流程,
就是和上面相反,
- 输入原始的数据,通过编码器
encoder
进行编码 - 再通过封装器
muxer
进行封装。 - 在通过协议
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
因为我们只是播放视频,所以我们只需要播放流程中的protocol
、demuxer
和decoder
、parser
从上图信息,我们可以知道
- decoder 和 parser
我们需要的视频的decoder
为h264
,音频的decoder
为aac
。同时,我们回顾到parser通常和decoder
是成对出现的。那同样为parser
添加h264
和aac
- demuxer
因为我们的视频是mp4的,所以我们使用mp4
- protocol
最后,因为我们是需要播放本地的文件。所以需要添加file
协议
2.编写编译脚本
在原来的编译脚本上,添加上我们的裁剪的脚本
- 先关闭所有的模块
--disable-everything
- 打开需要的模块
--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.pngGreat!!!看起来很不错。压缩之后,才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
当遇到问题时,按图索骥,找到对应的问题发生的点,然后再去查找是不是有所遗漏,来解决问题。
网友评论