本文章是用ffmeg解码封装格式(如mp4)转换为yuv420p保存到本地,本文是结合雷霄骅博客ppt和某地方学习的一个笔记(说出来等下被认为做广告就尴尬了)
封装格式
视频编码数据
将封装格式解压后可以得到压缩过的音视频等.
将压缩过的视频解压后可以得到 视频像素数据(RGB,YUV等).常见的视频压缩格式有H.264, MPEG4等…
YUV420P格式介绍
YUV是视频像素格式,在压缩视频格式解压可以得到,YUV也有很多种格式.下面举例三种.
1. Y代表亮度
2. uv代表色度
因为人对亮度的敏感远大于色度
转载地址
用三个图来直观地表示采集的方式吧,以黑点表示采样该像素点的Y分量,以空心圆圈表示采用该像素点的UV分量
先记住下面这段话,以后提取每个像素的YUV分量会用到。
YUV 4:4:4采样,每一个Y对应一组UV分量。
YUV 4:2:2采样,每两个Y共用一组UV分量。
YUV 4:2:0采样,每四个Y共用一组UV分量。
上面大家看最后一幅图即可.
编译ffmpeg的so库
因为我们安卓是linux系统,所以我们下载到本地放入到linux系统编译后给安卓使用.
1下载压缩包放入linux并解压
下面是解压后目录
我们打开文件夹看看.
2编写脚本控制configure生成so
创建一个build_android.sh文件作为脚本
#!/bin/bash make clean export NDK=/usr/ndk/android-ndk-r10e export SYSROOT=$NDK/platforms/android-9/arch-arm/ export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64 export CPU=arm export PREFIX=$(pwd)/android/$CPU export ADDI_CFLAGS="-marm" ./configure --target-os=linux \ --prefix=$PREFIX --arch=arm \ --disable-doc \ --enable-shared \ --disable-static \ --disable-yasm \ --disable-symver \ --enable-gpl \ --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-ffserver \ --disable-doc \ --disable-symver \ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ --enable-cross-compile \ --sysroot=$SYSROOT \ --extra-cflags="-Os -fpic $ADDI_CFLAGS" \ --extra-ldflags="$ADDI_LDFLAGS" \ $ADDITIONAL_CONFIGURE_FLAG make clean make make install
注意上面的几个地方
#你NDK存放在linux的路径,如果你没有下载那么请自行下载 export NDK=/usr/ndk/android-ndk-r10e
# 输出编译后的so路径 $(pwd)是当前路径 export PREFIX=$(pwd)/android/$CPU
#你要编译版本 export CPU=arm
上传到服务器ffmpeg解压目录下.
修改解压后ffmpeg目录文件权限
上面的代码会执行修改ffmpeg目录和子目录的权限为可读可写可执行(子目录也要不然有坑)
最后执行我们的脚本(大约会进行10分钟)
编译完成后会在 ffmpeg目录下的android的arm下生成两个文件
一个是include文件夹 ,另一个是lib
1. include 包含编译生成的so对应头文件
2. lib 生成的so文件
我们打开lib目录查看:
解释:
libXXX.so.YYYY,DDD
上面XXX是so 功能类名
YYYY,DDDD是版本号.这样库在安卓是无法使用的
解决办法:
修改目录下configure文件部分内容如下
注释部分为原来内容
#注释的部分 #SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' #LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"' #SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' #SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)' #自己写的部分 SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)' SLIB_INSTALL_LINKS='$(SLIBNAME)'
修改之后重新 执行./build_android即可
Eclipse编译之旅
将上面的lib中的so文件(当然也可以将lib文件夹复制过去)拷贝到eclipse目录的jni下,将include文件夹也放入jin下
修改Android.mk文件
LOCAL_PATH := $(call my-dir) #ffmpeg lib include $(CLEAR_VARS) LOCAL_MODULE := avcodec LOCAL_SRC_FILES := libavcodec-56.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avdevice LOCAL_SRC_FILES := libavdevice-56.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avfilter LOCAL_SRC_FILES := libavfilter-5.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avformat LOCAL_SRC_FILES := libavformat-56.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avutil LOCAL_SRC_FILES := libavutil-54.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := postproc LOCAL_SRC_FILES := libpostproc-53.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := swresample LOCAL_SRC_FILES := libswresample-1.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := swscale LOCAL_SRC_FILES := libswscale-3.so #LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := DemoFFmepeg LOCAL_SRC_FILES := DemoFFmepeg.c LOCAL_LDLIBS+= -llog LOCAL_SHARED_LIBRARIES := avcodec avdevice avfilter avformat avutil postproc swresample swscale #下面这行是导入include文件中头文件,这样做我们就可以在项目中 #直接include"libscan/xxx.h"而不是include"include/libscan/xxx.h" LOCAL_C_INCLUDES := $(LOCAL_PATH)/include include $(BUILD_SHARED_LIBRARY)
我们先看看MainActivity.java 文件
package com.fmple.demoffmepeg; import java.io.File; import android.app.Activity; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.Menu; import android.view.MenuItem; public class MainActivity extends Activity {
static{ System.loadLibrary("DemoFFmepeg"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final File inputFile = new File(Environment.getExternalStorageDirectory(),"a.mov"); final File outFile = new File(Environment.getExternalStorageDirectory(),"b.yuv"); //由于是耗时操作 所以开了个线程 new Thread(){ public void run() { ffmpeg(inputFile.getAbsolutePath(), outFile.getAbsolutePath()); }; }.start(); } / * * @param input 视频文件的输入路径 * @param out 把视频文件解码成yuv格式输出路径 */ public native void ffmpeg(String input,String out); }
上面也没什么好说的,就是创建activity的时候调用我们的一个jni方法ffmpeg
#include <jni.h> #include<stdio.h> #include<android/log.h> //编码 #include "libavcodec/avcodec.h" //封装格式处理 #include "libavformat/avformat.h" //像素处理 #include "libswscale/swscale.h" #define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO," FMY",FORMAT,__VA_ARGS__); #define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"FMY",FORMAT,__VA_ARGS__); char * output_cstr; / * 这里提供了另一种方式转化为saveYUV420 本案例不用 大家可以自己体验下此方法(这个方法在网上看到过) */ static void saveYUV420P(unsigned char *buf,int wrap,int xsize, int ysize){ FILE *fileOut = NULL; int i; if(buf ==NULL){ LOGE("缓存为空"); return ; } fileOut = fopen(output_cstr,"ab+"); for (i = 0;i<ysize; ++i) { fwrite(buf+i*wrap,1,xsize,fileOut); } fflush(fileOut); fclose(fileOut); } JNIEXPORT void JNICALL Java_com_fmple_demoffmepeg_MainActivity_ffmpeg (JNIEnv * env, jobject jobj, jstring input, jstring out){ char * input_char = (*env)->GetStringUTFChars(env,input,NULL); output_cstr = (*env)->GetStringUTFChars(env,out,NULL); //头文件libavformat/avformat.h //注册所有组件 / * 初始化libavformat和注册所有的 muxers, demuxers 和协议,如果你不想使用次函数, * 则可以手动选择你想要的支持格式 * 详情你可以选择下面的函数查询 * @see av_register_input_format() * @see av_register_output_format() * * muxer是合并将视频文件、音频文件和字幕文件合并为某一个视频格式。如,可将a.avi, a.mp3, a.srt用muxer合并为mkv格式的视频文件。 * demuxer是拆分这些文件的。 */ av_register_all(); // 封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息。 AVFormatContext * pFormatCtx = avformat_alloc_context(); / * 打开输入流并且读取头信息。但解码器没有打开 * 这个输入流必须使用avformat_close_input()关闭 * @param ps(第一个参数的形参名称) 指向 你由你提供AVFormatContext(AVFormatContext是由avformat_alloc_context函数分配的)。 * 有可能ps指向空,在这种情况下,AVFormatContext由此函数分配并写入ps。 * 注意: 你提供的AVFormatContext在函数执行失败的时候将会被释放 * @param url 你要打开视频文件路径. * @param fmt 如果不为空,那么这个参数将强制作为输入格式,否则自动检索 * @param options 一个关于AVFormatContext and demuxer-private 选项的字典. * 返回时,此参数将被销毁,并替换为包含未找到的选项的dict。有可能是空的 * * @return 返回0表示成功, 一个负数常量AVERROR是失败的. * * @note 如果你想自定义IO,你需要预分配格式内容并且设置pd属性 */ if(avformat_open_input(&pFormatCtx,input_char,NULL,NULL)!=0){ LOGE("NDK>>>%s","avformat_open_input打开失败"); return; } //上面打开输入流后会将视频封装格式信息写入AVFormatContext中那么我们下一步就可以得到一些展示信息 / * * 读取媒体文件中的数据包以获取流信息,这个对于对于文件格式没有头信息的很有帮助,比如说mpeg * 这个函数还可以计算在MPEG-2重复帧模式的真实帧速率。 * 逻辑文件位置不会被这个函数改变 * 检索过的数据包或许会缓存以供后续处理 * @param ic 第一个参数 封装格式上下文 * @param options * 如果不为空, 一个长度为 ic.nb_streams (封装格式所有流,字幕 视频 音频等) 的字典。 * 字典中第i个成员 包含一个对应ic第i个流中对的编码器。 * 在返回时,每个字典将会填充没有找到的选项 * @return 如果返回>=0 代表成功, AVERROR_xxx 表示失败 * * @note 这个函数 不保证能打开所有编码器,所以返回一个非空的选项是一个完全正常的行为 * * * @todo * 下个版本目标无视即可 * Let the user decide somehow what information is needed so that * we do not waste time getting stuff the user does not need. */ if( avformat_find_stream_info(pFormatCtx,NULL)<0){ LOGE("NDK>>>%s","avformat_find_stream_info失败"); return ; } LOGE("NDK>>>%s","成功"); // //输出视频信息 // LOGI("视频的文件格式:%s",pFormatCtx->iformat->name); // LOGI("视频时长:%d", (pFormatCtx->duration)/); //获取视频流的索引位置 //遍历所有类型的流(音频流、视频流、字幕流),找到视频流 int v_stream_idx = -1; int i = 0; //遍历封装格式中所有流 for (; i < pFormatCtx->nb_streams; ++i) { //获取视频流pFormatCtx->streams[i] //pFormatCtx->streams[i]->codec获取编码器 //codec_type获取编码器类型 //当前流等于视频 记录下标 if (pFormatCtx->streams[i]->codec->codec_type ==AVMEDIA_TYPE_VIDEO) { v_stream_idx = i; break; } } if (v_stream_idx==-1) { LOGE("没有找视频流") }else{ LOGE("找到视频流") } //编码器上下文结构体,保存了视频(音频)编解码相关信息 //得到视频流编码器 AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec; // 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。 AVCodec *pCodec =avcodec_find_decoder(pCodecCtx->codec_id); //(迅雷看看,找不到解码器,临时下载一个解码器) if (pCodec == NULL) { LOGE("%s","找不到解码器\n"); return; }else{ LOGE("%s","找到解码器\n"); } //打开解码器 / * 初始化 指定AVCodecContext去使用 给定的AVCodec * 在使用之前函数必须使用avcodec_alloc_context3()分配上下文。 * * 以下函数 avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(), * avcodec_find_decoder() and avcodec_find_encoder() 提供了一个简便的得到一个解码器的方法 * * @warning 这个函数线程不是安全的 * * @note 在使用解码程序之前,始终调用此函数 (如 @ref avcodec_decode_video2()). * 下面是示例代码 * @code * avcodec_register_all(); * av_dict_set(&opts, "b", "2.5M", 0); * codec = avcodec_find_decoder(AV_CODEC_ID_H264); * if (!codec) * exit(1); * * context = avcodec_alloc_context3(codec); * * if (avcodec_open2(context, codec, opts) < 0) * exit(1); * @endcode * * * @param avctx 要初始化的编码器 * @param codec 用这个codec去打开给定的上下文编码器.如果 codec 不为空 那么必须 * 事先用avcodec_alloc_context3和avcodec_get_context_defaults3传递给这个context,那么这个codec * 要么为NULL要么就是上面调用函数所使用的codec * * @param * * 选项填充AVCodecContext和编解码器私有选项的字典。返回时,此对象将填充未找到的选项。 * * @return 返回0表示成功, 负数失败 * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(), * av_dict_set(), av_opt_find(). */ if(avcodec_open2(pCodecCtx,pCodec,NULL)==0){ LOGE("%s","打开编码器成功\n"); }else{ LOGE("%s","打开编码器失败\n"); return; } //输出视频信息 LOGE("视频的文件格式:%s",pFormatCtx->iformat->name); //得到视频播放时长 if(pFormatCtx->duration != AV_NOPTS_VALUE){ int hours, mins, secs, us; int64_t duration = pFormatCtx->duration + 5000; secs = duration / AV_TIME_BASE; us = duration % AV_TIME_BASE; mins = secs / 60; secs %= 60; hours = mins/ 60; mins %= 60; LOGE("%02d:%02d:%02d.%02d\n", hours, mins, secs, (100 * us) / AV_TIME_BASE); } LOGE("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height); LOGE("解码器的名称:%s",pCodec->name); //存储一帧压缩编码数据。 AVPacket *packet =av_malloc(sizeof(AVPacket)); //输出转码文件地址 FILE *fp_yuv = fopen(output_cstr,"wb+"); //AVFrame用于存储解码后的像素数据(YUV) //内存分配 AVFrame *pFrame = av_frame_alloc(); //YUV420转码用 AVFrame *pFrameYUV = av_frame_alloc(); //avpicture_get_size()函数介绍: // / * 如果给定存储图片的格式,那么计算给定的宽高所占用的大小 * * @param pix_fmt 图片像素格式 * @param width 图片宽 * @param height 图片高 * @return 返回计算的图片缓存大小或者错误情况下的负数错误代码 * * * 这里计算缓存区的大小,但是没有分配,这里是用来后面转码使用 */ uint8_t *out_buffer = av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P,pCodecCtx->width,pCodecCtx->height)); //初始化缓冲区 / * 基于指定的图片参数和提供的图片缓存区去设置图片字段 * * 使用ptr所指向的图片数据缓存 填充图片属性 * * 如果 ptr是空,这个函数仅填充图片行大小(linesize)的数组并且返回图片缓存请求的大小 * * 要分配图片缓存并且再一次填充图片数据请使用 avpicture_alloc(). * @param picture 要填充的图片 * @param ptr 存储图片的数据的缓存区, or NULL * @param pix_fmt 图片像素格式 * @param width 图片宽 * @param height 图片高 * @return 返回请求的字节大小,在错误的情况下返回负数 * * @see av_image_fill_arrays() */ avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等 / *分配和返回 SwsContext. You need it to perform * scaling/conversion operations using sws_scale(). * * @param srcW 原始图宽 * @param srcH 原始图高 * @param srcFormat 原始图格式 * @param dstW 目标图宽 * @param dstH 不解释 * @param dstFormat 不解释 * @param flags 指定一个标志用于重新调整算法和选项 * 具体参考:http://blog.csdn.net/leixiaohua1020/article/details/ * @return 一个指定分配内容的指针, 错误情况返回空 * @note this function is to be removed after a saner alternative is * written */ 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; //读取每一帧 / *返回下一帧的流 * 此函数返回存储在文件中的内容,并且不会验证解码器有什么有效帧。 * 函数将存储在文件中的帧进行分割 并且返回给每一个调用者。 * * 函数不会删除在有效帧之间的无效数据 以便在可能解码过程中提供解码器最大的信息帮助 * 如果 pkt->buf 是空的,那么这个对应数据包是有效的直到下一次调用av_read_frame() * 或者直到使用avformat_close_input().否则包无期限有效 * 在这两种情况下 这个数据包当你不在需要的时候,你必须使用使用av_free_packet释放它 * 对于视屏,数据包刚好只包含一帧.对于音频,如果它每一帧是一个已知固定大小的,那么他包含整数帧(如. PCM or ADPCM data) * 如果音频帧具有可变大小(如. MPEG audio),那么他只包含一帧 * pkt->pts, pkt->dts and pkt->duration 始终在AVStream.time_base 单位设置正确的数值 *(如果这个格式无法提供.那么进行猜测) * 如果视频格式有B帧那么pkt->pts可以是 AV_NOPTS_VALUE.如果你没有解压他的有效部分那么最好依靠pkt->dts * * @return 0表示成功, < 0 错误或者文结束 */ while(av_read_frame(pFormatCtx,packet)>=0){ //一个包里有很多种类型如音频视频等 所以判断 这个包对应的流的在封装格式的下表 //如果这个包是视频频包那么得到压缩的视频包 if (packet->stream_index==v_stream_idx) { LOGE("测试"); / * 解码视频帧 从avpkt->data读取数据并且解码avpkt->size的大小后转化为图片. * 一些解码器可以支持在一个ACpacket中存在多帧的情况,像这样的解码器将只解码第一帧 * * @warning 输入缓存区必须 实际读取的字节流小于 FF_INPUT_BUFFER_PADDING_SIZE, * 一些优化过的比特流 读取32位或者64字节 的时候可以一次性读取完 * * @warning 在缓冲器的buf结尾设置0以确保被破坏的MPEG流不会发生超线程 * * @note 有 CODEC_CAP_DELAY 才能设置一个在输入和输出之间的延迟,这些需要使用avpkt->data=NULL, * 在结束返回剩余帧数的时候avpkt->size=0 * * @note 这个AVCodecContext 在数据包传入解码器之前必须调用avcodec_open2 * * * @param avctx 解码器上下文 * * @param[out] 解码的视频帧图片将会被存储在AVFrame. * 使用av_frame_alloc 得到一个AVFrame, * 编码器将会分配 使用 AVCodecContext.get_buffer2() 回调 * 的实际图片的内存. * 当AVCodecContext.refcounted_frames 设置为1,这帧(frame)是引用计数,并且返回 * 的引用计数是属于调用者的. * frame在长实际不使用的时候调用者必须调用av_frame_unref()就行释放 * 如果av_frame_is_writable()返回1那么调用者可以安全的写入到这个frame中。 * 当AVCodecContext.refcounted_frames设置为0,返回的引用属于解码器, * 只有下次使用这个函数或者关闭或者刷新这个编码器之前有效。调用者不会写入它 * *@param[in,out] got_picture_ptr 如果为0表示不能解压, 否者它不是0. * * @param[in] avpkt 这个输入的avpkt包含输入缓存区 * 你能使用av_init_packet()创建像这样的packet然后设置数据和大小, * 一些解码器还可以添加一些其他字段 比如 flags&AV_PKT_FLAG_KEY flags&AV_PKT_FLAG_KEY * 所有解码器都设计为尽可能少地使用 * * @return 再错误时返回一个负数 , 否则返回使用字节数或者或者0(没有帧被解压返回0)otherwise the number of bytes * */ ret=avcodec_decode_video2(pCodecCtx,pFrame,&got_picture,packet); if(ret>=0){ LOGE("解压成功"); //AVFrame转为像素格式YUV420,宽高 //2 6输入、输出数据 //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的 //4 输入数据第一列要转码的位置 从0开始 //5 输入画面的高度 sws_scale(sws_ctx,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); } } av_free_packet(packet); } (*env)->ReleaseStringUTFChars(env,input,input_char); (*env)->ReleaseStringUTFChars(env,out,output_cstr); //关闭文件 fclose(fp_yuv); //关闭资源 av_frame_free(&pFrame); av_frame_free(&pFrameYUV); //关闭编码器上下文 avcodec_close(pCodecCtx); //关闭输入流 avformat_close_input(&pFormatCtx); //关闭封装格式 avformat_free_context(pFormatCtx); }
Android Studio编译
注意:创建项目的时候勾选Include C++ support
导入所有so库到libs中
将include头文件导入cpp目录下
修改CMakeLists.txt文件
# For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.4.1) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. set(distribution_DIR ${CMAKE_SOURCE_DIR}/libs/) #libavcodec-56.so add_library(libavcodec-56-lib SHARED IMPORTED) set_target_properties(libavcodec-56-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libavcodec-56.so) #libavdevice-56.so add_library(libavdevice-56-lib SHARED IMPORTED) set_target_properties(libavdevice-56-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libavdevice-56.so) #libavfilter-5.so add_library(libavfilter-5-lib SHARED IMPORTED) set_target_properties(libavfilter-5-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libavfilter-5.so) #libavformat-56.so add_library(libavformat-56-lib SHARED IMPORTED) set_target_properties(libavformat-56-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libavformat-56.so) #libavutil-54.so add_library(libavutil-54-lib SHARED IMPORTED) set_target_properties(libavutil-54-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libavutil-54.so) #libpostproc-53.so add_library(libpostproc-53-lib SHARED IMPORTED) set_target_properties(libpostproc-53-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libpostproc-53.so) #libswresample-1.so add_library(libswresample-1-lib SHARED IMPORTED) set_target_properties(libswresample-1-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libswresample-1.so) #libswscale-3.so add_library(libswscale-3-lib SHARED IMPORTED) set_target_properties(libswscale-3-lib PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/${ANDROID_ABI}/libswscale-3.so) add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/native-lib.c ) # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. include_directories( src/main/cpp/include/ ) target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library # included in the NDK. ${log-lib} libavcodec-56-lib libavdevice-56-lib libavfilter-5-lib libavformat-56-lib libavutil-54-lib libpostproc-53-lib libswresample-1-lib libswscale-3-lib )
最后代码实现
#include <jni.h> #include<android/log.h> //编码 #include "libavcodec/avcodec.h" //封装格式处理 #include "libavformat/avformat.h" //像素处理 #include "libswscale/swscale.h" #define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO," FMY",FORMAT,__VA_ARGS__); #define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"FMY",FORMAT,__VA_ARGS__); JNIEXPORT void JNICALL Java_com_example_demoffmpeg_MainActivity_ffmpeg(JNIEnv * env, jobject jobj, jstring input, jstring out) { char * input_char = (*env)->GetStringUTFChars(env,input,NULL); char * output_cstr = (*env)->GetStringUTFChars(env,out,NULL); //头文件libavformat/avformat.h //注册所有组件 / * 初始化libavformat和注册所有的 muxers, demuxers 和协议,如果你不想使用次函数, * 则可以手动选择你想要的支持格式 * 详情你可以选择下面的函数查询 * @see av_register_input_format() * @see av_register_output_format() * * muxer是合并将视频文件、音频文件和字幕文件合并为某一个视频格式。如,可将a.avi, a.mp3, a.srt用muxer合并为mkv格式的视频文件。 * demuxer是拆分这些文件的。 */ av_register_all(); // 封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息。 AVFormatContext * pFormatCtx = avformat_alloc_context(); / * 打开输入流并且读取头信息。但解码器没有打开 * 这个输入流必须使用avformat_close_input()关闭 * @param ps(第一个参数的形参名称) 指向 你由你提供AVFormatContext(AVFormatContext是由avformat_alloc_context函数分配的)。 * 有可能ps指向空,在这种情况下,AVFormatContext由此函数分配并写入ps。 * 注意: 你提供的AVFormatContext在函数执行失败的时候将会被释放 * @param url 你要打开视频文件路径. * @param fmt 如果不为空,那么这个参数将强制作为输入格式,否则自动检索 * @param options 一个关于AVFormatContext and demuxer-private 选项的字典. * 返回时,此参数将被销毁,并替换为包含未找到的选项的dict。有可能是空的 * * @return 返回0表示成功, 一个负数常量AVERROR是失败的. * * @note 如果你想自定义IO,你需要预分配格式内容并且设置pd属性 */ if(avformat_open_input(&pFormatCtx,input_char,NULL,NULL)!=0){ LOGE("NDK>>>%s","avformat_open_input打开失败"); return; } //上面打开输入流后会将视频封装格式信息写入AVFormatContext中那么我们下一步就可以得到一些展示信息 / * * 读取媒体文件中的数据包以获取流信息,这个对于对于文件格式没有头信息的很有帮助,比如说mpeg * 这个函数还可以计算在MPEG-2重复帧模式的真实帧速率。 * 逻辑文件位置不会被这个函数改变 * 检索过的数据包或许会缓存以供后续处理 * @param ic 第一个参数 封装格式上下文 * @param options * 如果不为空, 一个长度为 ic.nb_streams (封装格式所有流,字幕 视频 音频等) 的字典。 * 字典中第i个成员 包含一个对应ic第i个流中对的编码器。 * 在返回时,每个字典将会填充没有找到的选项 * @return 如果返回>=0 代表成功, AVERROR_xxx 表示失败 * * @note 这个函数 不保证能打开所有编码器,所以返回一个非空的选项是一个完全正常的行为 * * * @todo * 下个版本目标无视即可 * Let the user decide somehow what information is needed so that * we do not waste time getting stuff the user does not need. */ if( avformat_find_stream_info(pFormatCtx,NULL)<0){ LOGE("NDK>>>%s","avformat_find_stream_info失败"); return ; } LOGE("NDK>>>%s","成功"); // //输出视频信息 // LOGI("视频的文件格式:%s",pFormatCtx->iformat->name); // LOGI("视频时长:%d", (pFormatCtx->duration)/); //获取视频流的索引位置 //遍历所有类型的流(音频流、视频流、字幕流),找到视频流 int v_stream_idx = -1; int i = 0; //遍历封装格式中所有流 for (; i < pFormatCtx->nb_streams; ++i) { //获取视频流pFormatCtx->streams[i] //pFormatCtx->streams[i]->codec获取编码器 //codec_type获取编码器类型 //当前流等于视频 记录下标 if (pFormatCtx->streams[i]->codec->codec_type ==AVMEDIA_TYPE_VIDEO) { v_stream_idx = i; break; } } if (v_stream_idx==-1) { LOGE("没有找视频流") }else{ LOGE("找到视频流") } //编码器上下文结构体,保存了视频(音频)编解码相关信息 //得到视频流编码器 AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec; // 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。 AVCodec *pCodec =avcodec_find_decoder(pCodecCtx->codec_id); //(迅雷看看,找不到解码器,临时下载一个解码器) if (pCodec == NULL) { LOGE("%s","找不到解码器\n"); return; }else{ LOGE("%s","找到解码器\n"); } //打开解码器 / * 初始化 指定AVCodecContext去使用 给定的AVCodec * 在使用之前函数必须使用avcodec_alloc_context3()分配上下文。 * * 以下函数 avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(), * avcodec_find_decoder() and avcodec_find_encoder() 提供了一个简便的得到一个解码器的方法 * * @warning 这个函数线程不是安全的 * * @note 在使用解码程序之前,始终调用此函数 (如 @ref avcodec_decode_video2()). * 下面是示例代码 * @code * avcodec_register_all(); * av_dict_set(&opts, "b", "2.5M", 0); * codec = avcodec_find_decoder(AV_CODEC_ID_H264); * if (!codec) * exit(1); * * context = avcodec_alloc_context3(codec); * * if (avcodec_open2(context, codec, opts) < 0) * exit(1); * @endcode * * * @param avctx 要初始化的编码器 * @param codec 用这个codec去打开给定的上下文编码器.如果 codec 不为空 那么必须 * 事先用avcodec_alloc_context3和avcodec_get_context_defaults3传递给这个context,那么这个codec * 要么为NULL要么就是上面调用函数所使用的codec * * @param * * 选项填充AVCodecContext和编解码器私有选项的字典。返回时,此对象将填充未找到的选项。 * * @return 返回0表示成功, 负数失败 * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(), * av_dict_set(), av_opt_find(). */ if(avcodec_open2(pCodecCtx,pCodec,NULL)==0){ LOGE("%s","打开编码器成功\n"); }else{ LOGE("%s","打开编码器失败\n"); return; } //输出视频信息 LOGE("视频的文件格式:%s",pFormatCtx->iformat->name); //得到视频播放时长 if(pFormatCtx->duration != AV_NOPTS_VALUE){ int hours, mins, secs, us; int64_t duration = pFormatCtx->duration + 5000; secs = duration / AV_TIME_BASE; us = duration % AV_TIME_BASE; mins = secs / 60; secs %= 60; hours = mins/ 60; mins %= 60; LOGE("%02d:%02d:%02d.%02d\n", hours, mins, secs, (100 * us) / AV_TIME_BASE); } LOGE("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height); LOGE("解码器的名称:%s",pCodec->name); //存储一帧压缩编码数据。 AVPacket *packet =av_malloc(sizeof(AVPacket)); //输出转码文件地址 FILE *fp_yuv = fopen(output_cstr,"wb+"); //AVFrame用于存储解码后的像素数据(YUV) //内存分配 AVFrame *pFrame = av_frame_alloc(); //YUV420转码用 AVFrame *pFrameYUV = av_frame_alloc(); //avpicture_get_size()函数介绍: // / * 如果给定存储图片的格式,那么计算给定的宽高所占用的大小 * * @param pix_fmt 图片像素格式 * @param width 图片宽 * @param height 图片高 * @return 返回计算的图片缓存大小或者错误情况下的负数错误代码 * * * 这里计算缓存区的大小,但是没有分配,这里是用来后面转码使用 */ uint8_t *out_buffer = av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P,pCodecCtx->width,pCodecCtx->height)); //初始化缓冲区 / * 基于指定的图片参数和提供的图片缓存区去设置图片字段 * * 使用ptr所指向的图片数据缓存 填充图片属性 * * 如果 ptr是空,这个函数仅填充图片行大小(linesize)的数组并且返回图片缓存请求的大小 * * 要分配图片缓存并且再一次填充图片数据请使用 avpicture_alloc(). * @param picture 要填充的图片 * @param ptr 存储图片的数据的缓存区, or NULL * @param pix_fmt 图片像素格式 * @param width 图片宽 * @param height 图片高 * @return 返回请求的字节大小,在错误的情况下返回负数 * * @see av_image_fill_arrays() */ avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等 / *分配和返回 SwsContext. You need it to perform * scaling/conversion operations using sws_scale(). * * @param srcW 原始图宽 * @param srcH 原始图高 * @param srcFormat 原始图格式 * @param dstW 目标图宽 * @param dstH 不解释 * @param dstFormat 不解释 * @param flags 指定一个标志用于重新调整算法和选项 * 具体参考:http://blog.csdn.net/leixiaohua1020/article/details/ * @return 一个指定分配内容的指针, 错误情况返回空 * @note this function is to be removed after a saner alternative is * written */ 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; //读取每一帧 / *返回下一帧的流 * 此函数返回存储在文件中的内容,并且不会验证解码器有什么有效帧。 * 函数将存储在文件中的帧进行分割 并且返回给每一个调用者。 * * 函数不会删除在有效帧之间的无效数据 以便在可能解码过程中提供解码器最大的信息帮助 * 如果 pkt->buf 是空的,那么这个对应数据包是有效的直到下一次调用av_read_frame() * 或者直到使用avformat_close_input().否则包无期限有效 * 在这两种情况下 这个数据包当你不在需要的时候,你必须使用使用av_free_packet释放它 * 对于视屏,数据包刚好只包含一帧.对于音频,如果它每一帧是一个已知固定大小的,那么他包含整数帧(如. PCM or ADPCM data) * 如果音频帧具有可变大小(如. MPEG audio),那么他只包含一帧 * pkt->pts, pkt->dts and pkt->duration 始终在AVStream.time_base 单位设置正确的数值 *(如果这个格式无法提供.那么进行猜测) * 如果视频格式有B帧那么pkt->pts可以是 AV_NOPTS_VALUE.如果你没有解压他的有效部分那么最好依靠pkt->dts * * @return 0表示成功, < 0 错误或者文结束 */ while(av_read_frame(pFormatCtx,packet)>=0){ //一个包里有很多种类型如音频视频等 所以判断 这个包对应的流的在封装格式的下表 //如果这个包是视频频包那么得到压缩的视频包 if (packet->stream_index==v_stream_idx) { LOGE("测试"); / * 解码视频帧 从avpkt->data读取数据并且解码avpkt->size的大小后转化为图片. * 一些解码器可以支持在一个ACpacket中存在多帧的情况,像这样的解码器将只解码第一帧 * * @warning 输入缓存区必须 实际读取的字节流小于 FF_INPUT_BUFFER_PADDING_SIZE, * 一些优化过的比特流 读取32位或者64字节 的时候可以一次性读取完 * * @warning 在缓冲器的buf结尾设置0以确保被破坏的MPEG流不会发生超线程 * * @note 有 CODEC_CAP_DELAY 才能设置一个在输入和输出之间的延迟,这些需要使用avpkt->data=NULL, * 在结束返回剩余帧数的时候avpkt->size=0 * * @note 这个AVCodecContext 在数据包传入解码器之前必须调用avcodec_open2 * * * @param avctx 解码器上下文 * * @param[out] 解码的视频帧图片将会被存储在AVFrame. * 使用av_frame_alloc 得到一个AVFrame, * 编码器将会分配 使用 AVCodecContext.get_buffer2() 回调 * 的实际图片的内存. * 当AVCodecContext.refcounted_frames 设置为1,这帧(frame)是引用计数,并且返回 * 的引用计数是属于调用者的. * frame在长实际不使用的时候调用者必须调用av_frame_unref()就行释放 * 如果av_frame_is_writable()返回1那么调用者可以安全的写入到这个frame中。 * 当AVCodecContext.refcounted_frames设置为0,返回的引用属于解码器, * 只有下次使用这个函数或者关闭或者刷新这个编码器之前有效。调用者不会写入它 * *@param[in,out] got_picture_ptr 如果为0表示不能解压, 否者它不是0. * * @param[in] avpkt 这个输入的avpkt包含输入缓存区 * 你能使用av_init_packet()创建像这样的packet然后设置数据和大小, * 一些解码器还可以添加一些其他字段 比如 flags&AV_PKT_FLAG_KEY flags&AV_PKT_FLAG_KEY * 所有解码器都设计为尽可能少地使用 * * @return 再错误时返回一个负数 , 否则返回使用字节数或者或者0(没有帧被解压返回0)otherwise the number of bytes * */ ret=avcodec_decode_video2(pCodecCtx,pFrame,&got_picture,packet); if(ret>=0){ LOGE("解压成功"); //AVFrame转为像素格式YUV420,宽高 //2 6输入、输出数据 //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的 //4 输入数据第一列要转码的位置 从0开始 //5 输入画面的高度 sws_scale(sws_ctx,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); } } av_free_packet(packet); } (*env)->ReleaseStringUTFChars(env,input,input_char); (*env)->ReleaseStringUTFChars(env,out,output_cstr); //关闭文件 fclose(fp_yuv); //关闭资源 av_frame_free(&pFrame); av_frame_free(&pFrameYUV); //关闭编码器上下文 avcodec_close(pCodecCtx); //关闭输入流 avformat_close_input(&pFormatCtx); //关闭封装格式 avformat_free_context(pFormatCtx); }
Studio版本DEMO
今天的文章 安卓用ffmeg解码分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/83284.html