版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huanghuangjin/article/details/81876823
dts(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
pts(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
AVFrame的linesize成员:
代码
#include "common.hpp"
#ifdef __cplusplus // ffmpeg是基于c语言开发的库,所有头文件引入的时候需要 extern "C"
extern "C" {
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavcodec/jni.h"
JNIEXPORT jint JNI_OnLoad(JavaVM * vm, void * res) // jni 初始化时会调用此函数
{
// ffmpeg要使用硬解码,需要将java虚拟机环境传给ffmpeg,ffmpeg中通过vm来调用android中mediacodec硬解码的java接口 , 第二个函数为日志,不需要,传0
av_jni_set_java_vm(vm, 0);
return JNI_VERSION_1_4; // 选用jni 1.4 版本
}
#ifdef __cplusplus
}
#endif
// 当前时间戳
static long long getNowMs()
{
/*
struct timeval tv1,tv2;
unsigned long long timeconsumed = 0;
// 获取当前时间:
gettimeofday(&tv1,NULL);
...
gettimeofday(&tv2,NULL);
// 时间统计:
timeconsumed = tv2.tv_sec-tv1.tv_sec +(tv2.tv_usev-tv1.tv_usec)/1000000; // 以秒为单位
*/
struct timeval tv; // timeval结构体的两个成员为 秒 与 微秒
gettimeofday(&tv, NULL); // 获取系统当前时间, 1970到系统时间的秒数? signed long值装不下
int sec = tv.tv_sec%360000; // 100个小时
long long time = sec*1000 + tv.tv_usec/1000; // 将 timeval 时间单位转换为 毫秒
return time;
}
JNIEXPORT void JNICALL Java_hankin_hjmedia_mpeg_Mp5_11Activity_decode(JNIEnv *env, jobject instance, jstring url_, jobject handle)
{
const char * path = env->GetStringUTFChars(url_, NULL); // jstring 2 char*
if (path[0]=='\0')
{
LOGE("path is empty.");
return;
}
av_register_all();
int netInit = avformat_network_init();
LOGD("netInit=%d", netInit);
avcodec_register_all(); // 在4.0 已过时,Register all the codecs, parsers and bitstream filters which were enabled at configuration time. 注释此代码无影响
AVFormatContext * ic = NULL;
int openRet = avformat_open_input(&ic, path, NULL, NULL); // 打开媒体文件
if (openRet!=0 || ic==NULL)
{
LOGE("avformat_open_input is failed : %s", av_err2str(openRet));
return;
}
int findStream = avformat_find_stream_info(ic, NULL); // 查找媒体信息
if (findStream!=0)
{
LOGE("avformat_find_stream_info is failed");
return;
}
LOGD("duration=%lld, nb_streams=%d, bit_rate=%lld", ic->duration, ic->nb_streams, ic->bit_rate);
int videoStream = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); // 查找视频流在媒体流中的下标,方式一
int audioStream = -1;
for (int i = 0; i < ic->nb_streams; ++i)
{
AVStream * as = ic->streams[i];
if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) // 音频流
{
audioStream = i; // 查找音频流在媒体流中的下标,方式二,遍历
break;
}
}
if (videoStream==AVERROR_STREAM_NOT_FOUND || audioStream==-1)
{
LOGE("没有找到视频流或音频流 : videoStream=%d, audioStream=%d", videoStream, audioStream);
return;
}
LOGD("videoStream=%d, audioStream=%d", videoStream, audioStream);
AVPacket * pkt = av_packet_alloc(); // 创建AVPacket
/////////////////// 视频解码器配置信息 /////////////////// 打开解码器最好与解封装代码分开
AVCodec * videoCodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id); // 查找软解码器,AVCodec存放解码器的配置信息,并不是解码信息
if (!videoCodec) LOGE("查找视频软解码器失败.");
// 通过名字查找解码器,之前编译ffmpeg的时候将硬解码编译了进去,所以这里name可以传 "h264_mediacodec" 表示硬解,
// mediacodec是android arm芯片自带的硬解码固件,这里ffmpeg函数内部调用了android的java接口,因为android系统的mediacodec硬解只提供了java接口
videoCodec = avcodec_find_decoder_by_name("h264_mediacodec"); // 硬解码器
if (!videoCodec)
{
LOGE("查找视频硬解码器失败.");
return; // 不要return,还需要在后面释放相应内存
}
// 创建视频解码器上下文
AVCodecContext * videoContext = avcodec_alloc_context3(videoCodec); // 创建编解码器(ffmpeg中编码解码都是用的AVCodec),codec参数传NULL也可以,但是不会创建编、解码器
if (!videoContext)
{
LOGE("创建视频解码器上下文失败.");
return;
}
int cvRet = avcodec_parameters_to_context(videoContext, ic->streams[videoStream]->codecpar); // 将AVStream中的参数直接复制到AVCodec中
if (cvRet!=0) LOGE("视频 avcodec_parameters_to_context 错误.");
videoContext->thread_count = 8; // 设置软解码线程数量,硬解码时次参数无用
/*
int avcodec_open2(
AVCodecContext *avctx, // 编解码器上下文
const AVCodec *codec, // 如果在创建AVCodecContext的时候没有传codec,那么这里就要传,创建时传了,这里就可以传NULL,总之,这两个函数中只要传一个codec
AVDictionary **options // 字典的数组,key-value , 参看 ffmpeg源码 ./libavformat/options_table.h
);
*/
int vret = avcodec_open2(videoContext, 0, 0); // 打开视频解码器
if (vret!=0)
{
LOGE("打开视频解码器失败");
return;
}
/////////////////// 音频解码器,同视频解码器一样 ///////////////////
AVCodec * audioCodec = avcodec_find_decoder(ic->streams[audioStream]->codecpar->codec_id); // 音频没有硬解码器?
if (!audioCodec)
{
LOGE("查找音频软解码器失败.");
return;
}
AVCodecContext * audioContext = avcodec_alloc_context3(audioCodec);
if (!audioContext)
{
LOGE("创建音频解码器上下文失败.");
return;
}
int caRet = avcodec_parameters_to_context(audioContext, ic->streams[audioStream]->codecpar);
if (caRet!=0) LOGE("音频 avcodec_parameters_to_context 错误.");
audioContext->thread_count = 8;
int aret = avcodec_open2(audioContext, 0, 0);
if (aret!=0)
{
LOGE("打开音频解码器失败");
return;
}
AVFrame * frame = av_frame_alloc(); // 创建AVFrame
/////////////////// end ///////////////////
long long start = getNowMs(); // 其实时间
int frameCount = 0;
while (true)
{
if (getNowMs() - start >= 3000) // 3秒内
{
// 软解码单线程时,手机每秒50帧左右(非neon、mediacodec的so库性能差百分之三四十),模拟器差不多快一倍。手机cpu占用率大概在16%
// 8线程时性能几乎快一倍,但是cpu占用率有百分之七八十
// 硬解码时,使用的是非cpu gpu的固件,解码率理论上是固定的
LOGW("3秒内平均每秒解码视频帧数 : %d", frameCount/3);
start = getNowMs();
frameCount = 0;
}
int ret = av_read_frame(ic, pkt); // 读取视频流和音频流的每一帧数据,返回非0表示出错或者读到了文件末尾
if (ret!=0)
{
LOGD("读到结尾处了");
int pos = 20 * (double) ic->streams[videoStream]->time_base.num / (double) ic->streams[videoStream]->time_base.den;
av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME); // seek到在pos位置往前找到关键帧的地方
break;
}
LOGV("stream=%d, size=%d, pts=%lld, flag=%d", pkt->stream_index, pkt->size, pkt->pts, pkt->flags);
// 开始视频、音频解码
AVCodecContext * acc = videoContext;
if (pkt->stream_index==audioStream) acc = audioContext;
/*
int avcodec_send_packet( // 早前ffmpeg解码音视频是分两个不同接口,现在是多线程解码,需要先将AVPacket发到解码队列中去(子线程中)
AVCodecContext *avctx, // 解码上下文
const AVPacket *avpkt // 调用此函数会浅拷贝一份AVPacket,avpkt内部data引用计数会加1,所以调用此函数后可以放心 av_packet_unref与释放avpkt
); // return 0 on success, otherwise negative error code:
*/
int rr = avcodec_send_packet(acc, pkt); // 最后一帧的处理 pkt 要传 NULL ?
if (pkt->stream_index==videoStream) LOGI("%s send rr=%d", "video", rr);
else LOGD("%s send rr=%d", "audio", rr);
/*
AVFrame 结构体,存放每帧解码后的数据,对应着AVPacket,不过比AVPacket的内存开销更大
uint8_t *data[AV_NUM_DATA_POINTERS]; 指针数组,如果是视频表示每行的数据,如果是音频表示每个通道的数据。 这样存的原因是因为数据的存放方式(有交叉存放有平面存放)
int linesize[AV_NUM_DATA_POINTERS]; 与data成对的信息,如果是视频表示一行数据的大小,如果是音频表示一个通道的数据的大小
int width; 视频的宽高,只有视频有
int height;
int nb_samples; 单通道的样本数量,只有音频有
int64_t pts; 这一帧的pts
int64_t pkt_pts; 对应AVPacket中的pts
int64_t pkt_dts; 对应AVPacket中的dts
uint64_t channel_layout; 有音频有,通道类型,Channel layout of the audio data.
int channels; 声道数,只用于音频
int sample_rate; 采样率,只用于音频
int format; 像素格式(枚举AVPixelFormat) 或 音频的样本格式(比如16bit、32bit等)(枚举AVSampleFormat)
AVFrame *av_frame_alloc(void); 创建AVFrame
void av_frame_free(AVFrame **frame); 释放AVFrame本身的内存
AVFrame *av_frame_clone(const AVFrame *src); 同AVPacket的操作类似,浅拷贝,src内部data的引用计数加1
int av_frame_ref(AVFrame *dst, const AVFrame *src); 同AVPacket的操作类似,src浅拷贝到dst,同时src内部data的引用计数加1
void av_frame_unref(AVFrame *frame); 同AVPacket的操作类似,将frame内部data的引用计数减1 ,为0时释放data内存
int avcodec_receive_frame( // 从解码队列中已成功解码的数据的缓存中取出帧数据,一次函数调用有可能接受到多帧数据,也有可能接受不到
AVCodecContext *avctx, // 解码上下文
AVFrame *frame // 返回的接受到的每帧的数据
); // return 0 on success, otherwise negative error code:
*/
while (true) // 循环接受解码队列中成功解码的帧数据,直到该时刻没有成功解码的帧数据为止
{
rr = avcodec_receive_frame(acc, frame);
if (pkt->stream_index==videoStream) LOGI("%s receive rr=%d", "video", rr);
else LOGD("%s receive rr=%d", "audio", rr);
if (rr==0)
{
if (pkt->stream_index==videoStream) LOGI("%s frame->pts=%lld", "video", frame->pts);
else LOGD("%s frame->pts=%lld", "audio", frame->pts);
}
else break; // 没有接收到的时候,break,这个时候是不会为frame内部的data申请内存的
av_frame_unref(frame); // 这句代码不加也不会内存溢出。。 avcodec_receive_frame 并不会为 frame 的data重复申请内存?
if (pkt->stream_index==videoStream && rr==0) frameCount++; // 读取到的视频帧数 ++
}
av_packet_unref(pkt); // av_read_frame读取每帧数据的时候会为AVPacket的data指针申请内存,这里将其引用计数减1,以释放内存
}
avcodec_free_context(&videoContext); // 清理AVCodecContext内存空间,如果不及时释放会造成应用内存崩溃
avcodec_free_context(&audioContext);
av_packet_free(&pkt); // 释放AVPacket本身占用的内存
avformat_close_input(&ic); // 关闭打开的媒体流,释放内存
env->ReleaseStringUTFChars(url_, path); // java的String引用计数减1,释放path内存
}