版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huanghuangjin/article/details/81840807
#include "common.hpp"
#ifdef __cplusplus // ffmpeg是基于c语言开发的库,所有头文件引入的时候需要 extern "C"
extern "C" {
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#ifdef __cplusplus
}
#endif
static bool isRead = true; // 用来退出循环解封装
static double r2d(AVRational r) // 时间基数转换为double
{
return r.num==0||r.den==0 ? 0 : (double)r.num/(double)r.den;
}
JNIEXPORT jstring JNICALL Java_hankin_hjmedia_mpeg_Mp3_11Activity_ffmpegConfigure(JNIEnv *env, jobject instance)
{
const char * config = avcodec_configuration(); // 获取编译ffmpeg时的./configure命令信息
LOGD2("mydebug---", "编译ffmpeg时的./configure命令信息 : %s", config);
return charTojstring_hjmedia(env, config);
}
JNIEXPORT jboolean JNICALL Java_hankin_hjmedia_mpeg_Mp3_11Activity_open(JNIEnv *env, jobject instance, jstring url_, jobject handle) // 如果在java层面url_传null,会报错
{
jboolean jb;
// 当从 JNI 函数 GetStringUTFChars 中返回得到字符串B时,如果B是原始字符串java.lang.String 的拷贝,则isCopy被赋值为 JNI_TRUE,(结果是拷贝)
// 如果B和原始字符串指向的是JVM中的同一份数据,则 isCopy被赋值为 JNI_FALSE。
const char *path = env->GetStringUTFChars(url_, &jb); // java中UTF字符集的字符转换为char*,不会有中文乱码
LOGV("jb=%d, JNI_FALSE=%d", jb, JNI_FALSE); // jb=1, JNI_FALSE=0
LOGI("paht=%s", path);
if (path[0]=='\0') // 空字符传 ""
{
LOGE("path is empty.");
return JNI_FALSE;
}
//初始化解封装
av_register_all(); // 注册所有格式,解封装、压封装,也可单个注册某个格式 注释此代码对后面没影响
//初始化网络
avformat_network_init(); // 注册支持rtsp格式的数据(即基于rtp协议的),也支持http格式的数据 注释此代码对后面似乎也没影响
//打开文件
/*
AVFormatContext // 结构体,封装的上下文,用于解封装、压封装
AVIOContext *pb; io流context,如果是自定义的格式,需要使用这个自己到内存中读
char filename[1024]; 文件地址
unsigned int nb_streams; 存放AVStream的大小(即流的数量),一般就2个(flv文件获取不到),视频音频,可能还会有字幕
AVStream **streams; 是一个数组,存的视频音频流,一般0是视频下标,1是音频下标,最好还是遍历查找确定一下
int64_t duration; 整个媒体文件的总长度,以时间基数 AV_TIME_BASE(一百万分之一秒) 为单位,
在AVStream中也有时间,这两个时间不一定相等。duration也不能保证一定获取到(比如flv文件获取不到)
int64_t bit_rate; 比特率,1秒中内文件的大小
*/
AVFormatContext * context = NULL;
/*
int avformat_open_input( // 打开某格式的音视频, 调用前确保已经 av_register_all 或 avformat_network_init 注册
AVFormatContext **ps, // 格式化的上下文,**ps可以为NULL(ffmpeg会自动创建),但需要将指向NULL的指针的地址 ps 传进来,不能传NULL,会出错
const char *url, // 音视频文件地址,支持本地、网络 http rtsp 等,会存到 AVFormatContext 结构体中,可以用作断开重连
AVInputFormat *fmt, // 指定输入的封装格式,一般不用,传NULL就行,让ffmpeg自行探测,因为探测有消耗,如果需要频繁的打开的话还是传一下格式
AVDictionary **options // 字典的数组,key-value , 打开本地文件传NULL就行,如果是rtsp的,需要传递 参看 ffmpeg源码 ./libavformat/options_table.h
);
*/
int ret = avformat_open_input(&context, path, 0, 0); // 返回 0 表示打开成功
LOGD2("mydebug---", "context is NULL : %d", (context==NULL)); // path不对的时候,avformat_open_input就不会创建context了
if(ret!=0 || context==NULL)
{
LOGE2("mydebug---", "avformat_open_input open failed : %s", av_err2str(ret)); // av_err2str(ret)输出打开失败的原因
return JNI_FALSE;
}
// 1 duration=60000000, nb_streams=2, bit_rate=0
LOGD("1 duration=%lld, nb_streams=%u, bit_rate=%lld", context->duration, context->nb_streams, context->bit_rate);
// 如果打开音视频的时候,其头文件中没有包含格式和索引信息的时候,利用此函数到文件后面去查找。除了这个还有探测,直接探测网络上过来的流
ret = avformat_find_stream_info(context, 0); // 返回 0 表示找到了流的信息
if (ret!=0) LOGW2("mydebug---", "avformat_find_stream_info failed : %s", av_err2str(ret)); // 这个时候流信息读到了
// 2 duration=60000000, nb_streams=2, bit_rate=3447910
LOGW("2 duration=%lld, nb_streams=%u, bit_rate=%lld", context->duration, context->nb_streams, context->bit_rate);
// 打印音视频信息
double fps = 0;
int videoStream = 0; // 视频流的下标
int audioStream = 1; // 音频流的下标
for (int i = 0; i < context->nb_streams; ++i)
{
/*
AVStream 结构体,解封装、解码
AVCodecContext *codec; 已过时,以前是将解封装与解码放在一起的,新版本将它们隔离
AVRational time_base; 时间基数,Rational表示有理数(即能用分子分母表示的数),可以理解为一个分数,它里面存的就是分子分母
int64_t duration; 时长,不一定有 duration * ((double)time_base.num/(double)time_base.den) * 1000 此表达式得到的是毫秒
int64_t nb_frames;
AVRational avg_frame_rate; 平均帧率,主要针对视频,也是用的分数表示
AVCodecParameters *codecpar; 结构体,音视频参数,用来替代上面过时的 AVCodecContext *codec 的
enum AVMediaType codec_type; 编码数据的类型,音频还是视频
enum AVCodecID codec_id; 编码格式,在ffmpeg内部 H264 或者 其他编码 都对应着一种格式,参看枚举 AVCodecID
uint32_t codec_tag; 用4个字节来表示各种编码器,暂时用不到
int format; 像素格式(枚举AVPixelFormat) 或 音频的样本格式(比如16bit、32bit等)(枚举AVSampleFormat)
int width; 宽高,只有视频有
int height;
uint64_t channel_layout; 只有音频有,The channel layout bitmask. May be 0 if the channel layout is unknown or unspecified,
otherwise the number of bits set must be equal to the channels field
int channels; 声道数,只用于音频
int sample_rate; 采样率,只用于音频
int frame_size; 一帧音频的大小,只用于音频
*/
AVStream * as = context->streams[i]; // 获取文件中流信息
if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) // 视频数据
{
videoStream = i; // 保存视频流的下标
fps = r2d(as->avg_frame_rate); // 获取平均帧率
// 视频数据 : fps=25.000000, width=1920, height=1080, codec_id=27, pixformat=0, videoStream=0
LOGD("视频数据 : fps=%f, width=%d, height=%d, codec_id=%d, pixformat=%d, videoStream=%d",
fps, as->codecpar->width, as->codecpar->height, as->codecpar->codec_id, as->codecpar->format, videoStream);
}
else if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) // 音频数据
{
audioStream = i; // 保存音频流的下标
// 音频数据 : sample_rate=44100, channels=2, sample_format=8, audioStream=1 8表示AV_SAMPLE_FMT_FLTP(float平面类型)
LOGD("音频数据 : sample_rate=%d, channels=%d, sample_format=%d, audioStream=%d",
as->codecpar->sample_rate, as->codecpar->channels, as->codecpar->format, audioStream);
}
}
// 除了上面的遍历的方式获取流信息,还可以用以下方式
/*
int av_find_best_stream( // 打开音视频文件后,需要对音视频进行单独的处理,此函数返回音频或视频流在上下文的streams中的索引
AVFormatContext *ic, // 上下文
enum AVMediaType type, // 要获取的音频还是视频信息
int wanted_stream_nb, // 想要的流信息,暂传-1
int related_stream, // 相关的流信息,ffmpeg中视频下还有节目的概念,咱传-1
AVCodec **decoder_ret, // 解码器,传一个空指针进来,函数会自动分配内存,暂用不到,因为将解封装与解码分开了,咱传NULL
int flags // 预留参数,ffmpeg现在还没用到,传0即可
);
*/
audioStream = av_find_best_stream(context, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
LOGI2("mydebug---", "audioStream=%d", audioStream); // audioStream=1
//读取帧数据,并从视频20秒的位置到末尾循环读取,看内存情况 循环一定次数后logcat会read: unexpected EOF!,但是循环还是继续执行的
/*
AVPacket 结构体
AVBufferRef *buf; 指针,指向buffer空间,用于存储引用计数
int64_t pts; 显示的时间,具体为 pts * (num/den) 时间单位也是微秒(百万分之一秒)
int64_t dts; 解码的时间,具体为同上。 如果没有B帧,pts与dts是一致的
uint8_t *data; 指向的是 AVBufferRef *buf 中再分配的空间
int size; data的size
// AVPacket 的一些使用
AVPacket *av_packet_alloc(void); 创建并初始化AVPacket,这个会申请分配堆上内存,用完后需要释放
AVPacket *av_packet_clone(const AVPacket *src); 浅拷贝, AVPacket *src 的引用计数会加1
int av_packet_ref(AVPacket *dst, const AVPacket *src); 将 src 加到 dst 中,src 的引用计数会加1 ,该函数其实就是调用了上面的两个函数
void av_packet_unref(AVPacket *pkt); 将 AVPacket *pkt 的引用计数减1,引用计数为0的时候,AVPacket内部的 uint8_t *data 就会释放内存
void av_packet_free(AVPacket **pkt); 除了要释放AVPacket内部指针的指向的内存,还需要释放AVPacket本身占用的内存,
此函数释放AVPacket内存并减引用计数,同时*pkt指针会置空,防止野指针
void av_init_packet(AVPacket *pkt); 在栈上以默认值初始化AVPacket, this does not touch the data and size members
int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size); 初始化 AVPacket 中 data 与 size 成员
int av_copy_packet(AVPacket *dst, const AVPacket *src); 深拷贝,已过时,用 av_packet_clone 与引用计数代替
*/
AVPacket * pkt = av_packet_alloc();
while (isRead)
{
/*
int av_read_frame( // 读解封装出来的数据包,其包含了 pts dts 音频或视频索引 是否是关键帧 ,不过AVPacket中去掉了上面说的H264的间隔符00000001或000001
AVFormatContext *s, // 上下文
AVPacket *pkt // 解封装后出来的一个一个的数据包,不能传NULL
); // 返回值 0 表示ok,小于0表示错误或读到了末尾
*/
int result = av_read_frame(context, pkt); // 读取每帧的时候,pkt内部data指针会分配内存,如果不释放会造成内存泄漏,累计多了应用也会内存崩溃
if (result!=0)
{
LOGI("读到结尾处了");
/*
int av_seek_frame( // seek操作,可以单独的针对音频或视频独立操作seek, return >= 0 on success
AVFormatContext *s, // 封装格式的上下文
int stream_index, // 音频视频流的索引,-1 default 一般是视频
int64_t timestamp, // 时间戳,即要移动到哪个时间位置,单位为微秒,同样要以 AVStream.time_base 处理
int flags // 标识位,表示移动的方法,解决seek时非关键帧与时间戳的计算
AVSEEK_FLAG_BACKWARD 1 ///< seek backward 往后找一帧(时间越早的称为后),
AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes 用的不多,按文件的大小位置添砖
AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes 任意帧,就找seek位置最近的帧,不会去寻找关键帧
AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number 表示要找到关键帧,要与其他flags一起使用 |
);
*/
double timebase = r2d(context->streams[videoStream]->time_base);
int pos = 20 * timebase; // 视频总长60秒,计算第20秒的时间戳
int sek = av_seek_frame(context, videoStream, pos, AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME); // 跳转到第20秒
LOGI("timebase=%lf, sek=%d", timebase, sek); // timebase=0.000078, sek=0
continue;
}
// stream 标识此为音频或视频的AVPacket
LOGV2("mydebug---", "stream=%d, size=%d, pts=%lld, flag=%d", pkt->stream_index, pkt->size, pkt->pts, pkt->flags);
// 将 AVPacket *pkt 的引用计数减1,引用计数为0的时候,AVPacket内部的 uint8_t *data 就会释放内存
av_packet_unref(pkt);
}
av_packet_free(&pkt); // 还需要释放AVPacket本身占用的内存,此函数释放AVPacket内存并减引用计数,同时pkt指针会置空,防止野指针
// 专门的用来关闭输入的上下文的函数,关闭输出的有其他方式。函数内部已经将指针置零(包含AVFormatContext占用的空间与其内部成员指针指向的空间)了,防止野指针
avformat_close_input(&context);
LOGI("close and end");
env->ReleaseStringUTFChars(url_, path); // 在jvm中release对象url_ ,这里是完全释放内存,还是只是对象的引用计数减1 ? 同时释放c的字符串的内存?
return JNI_TRUE; // 在c中可以不return,但是c++中必须要return,否则运行报错
}
JNIEXPORT void JNICALL Java_hankin_hjmedia_mpeg_Mp3_11Activity_stop(JNIEnv *env, jobject instance)
{
isRead = false;
}