FFmpeg的解封装大概可分为以下九个步骤:
- 打开媒体地址(文件路径、视频URL)
- 查找媒体中的音视频流信息
- 根据流信息流个数循环查找
- 获取媒体流
- 从流获取编解码参数
- 根据参数获取编解码器
- 创建编解码器上下文
- 使用上面的参数初始化编解码器上下文
- 打开解码器
- 循环读取压缩数据AVPacket
回忆下上篇文章的图
主要代码
Java层代码
Player的方法设计基本是按照MediaPlayer的API设计的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class JJHPlayer implements IJJHPlayer {
static { System.loadLibrary("native-lib"); }
public JJHPlayer(Context context){ this.mContext = context; }
@Override public void setDataSource(String dataSource){ this.mDataSource = dataSource; }
@Override public void prepare(){ nativePrepare(mDataSource); }
private void onNativeCallErrorEvent(int eventCode, int extra, String message){ mHandler.post(new Runnable() { @Override public void run() { if(mOnErrorEventListener!=null) mOnErrorEventListener.onError(eventCode, extra, message); } }); } private void onNativeCallPrepared(){ mHandler.post(new Runnable() { @Override public void run() { if(mOnPrepareListener!=null) mOnPrepareListener.onPrepared(JJHPlayer.this); } }); }
private native void nativePrepare(String dataSource);
}
|
JNI部分代码
1 2 3 4 5 6 7 8 9 10
| extern "C" JNIEXPORT void JNICALL Java_cn_jiajunhui_lib_jjhplayer_JJHPlayer_nativePrepare(JNIEnv* env, jobject thiz, jstring data_source) { const char *_data_source = env->GetStringUTFChars(data_source, 0); auto *helper = new JNICallbackHelper(vm, env, thiz); player = new JJHPlayer(_data_source, helper); player->setRenderCallback(renderCallback); player->prepare(); env->ReleaseStringUTFChars(data_source, _data_source); }
|
主要的结构体及函数释义
结构体
AVFormatContext
1 2 3 4 5 6 7 8 9 10 11
| 1> AVIOContext *pb:文件IO的上下文,自定义格式时使用
2> char filename[1024]:保存打开的文件名,经常用到,例如断开重连
3> unsigned int nb_streams:流数量
4> AVStream **streams:具体流内容,通常只有视频、音频,偶尔也会有字幕之类的
5> int64_t duration:总长度,以AV_TIME_BASE(通常为1000000)为单位,相当于使用微秒(us)为单位,注意这个值不一定能够获取到,如果获取不到可以通过帧数计算
6> int64_t bit_rate:比特率 1s中有多少bit
|
AVStream
1 2 3 4 5 6 7 8 9
| 1> AVCodecContext *codec:解码器,该参数已经过时
2> AVRational time_base:时间基数,分数,通常分子=>1,分母=>9000
3> int64_t duration:时长,duration * (time_base.num / time_base.den) 需要考虑除零
4> AVRational avg_frame_rate:帧率,对于视频来说一帧数据就是一张图片,对于音频来说就是一定量的样本数,具体一帧数据存多少样本数由codecpar->frame_size决定
5> AVCodecParameters *codecpar:音视频参数,主要用于替代codec
|
AVCodecParameters
1 2 3 4 5 6 7 8 9 10 11
| 1> enum AVMediaType codec_type:编码类型,音频/视频
2> enum AVCodecID codec_id:编码格式,H264格式等
3> uint32_t codec_tag:用四个字节表示编码器,通常用不到
4> int format:视频像素格式/音频采样格式
5> int width, int height:视频宽高,仅视频有;不一定有,如果没有可以使用解码后的frame中的宽高
6> uint64_t channel_layout,int channels,int sample_rate,int frame_size:声道,如三声道(数值与channels二进制十进制数相同),声道数,样本率,样本大小(单通道样本数),仅音频有
|
AVPacket
1 2 3 4 5 6 7 8 9 10 11
| 1> AVBufferRef *buf:用于存储引用计数的一块空间,packet增加的时候引用计数+1,减少的时候引用计数-1
2> int64_t pts:显示时间
3> int64_t dts:解码时间
4> uint8_t *data, int size:由ffmpeg创建和删除(不同帧数据量不同),保存帧数据
注意:在没有B帧的情况下pts = dts
AVPacket相关函数:av_packet_alloc(创建并初始化一个packet)、av_packet_clone(复制并增加一次引用计数)、av_packet_ref(手动加一次引用)、av_packet_unref(手动减一次引用)、av_packet_free(空间清理)、av_init_packet(为packet设置默认值)、av_packet_from_data(给定数据生成一个packet)、av_copy_packet(废弃的函数,注意不要再使用)
|
函数
1> av_register_all:注册所有的解封装和加封装格式,新版本的ffmpeg已经废弃该函数,可以不需要再调用
2> avformat_network_init:支持网络rtsp/rtmp/http数据流
3> avformat_open_input:打开音视频文件
1 2 3 4 5 6 7 8 9
| @param AVFormatContext **ps:需要注意ps不能为空,*ps可以为空,当*ps为空,会在内部创建存储空间,如果不传空可在外部先创建好空间但清理需要在外部处理
@param const char *url:支持网络rtsp、http、本地路径
@AVInputFormat *fmt:输入文件的格式,通常不需要指定,不指定的情况下由ffmpeg自己检测输入文件格式
@AVDictionary **options:输入参数字典,具体有哪些参数可以参考源码ffmpeg-4.3.1\libavcodec\options_table.h里面的定义;可以使用方法av_dict_set设置参数,比如设置rtsp超时时间
@return:0表示正常,非0返回错误码
|
4> avformat_find_stream_info:获取流信息
5> av_dump_format:打印流详细信息
1 2 3 4 5 6 7
| @param AVFormatContext **ps
@param int index:用于打印(没啥用)
@param const char *url:用于打印(没啥用)
@param int is_output:context是输入(0)或输出(1)
|
6> av_find_best_stream:获取音视频流信息
1 2 3 4 5 6 7 8 9 10 11
| @param AVFormatContext **ps
@param enum AVMediaType type
@param int wanted_stream_nb:通常设为-1,自动选择
@param int ratedstream:相关流,通常用不到,设为-1
@param AVCodec **decoder_ret:解码时用到,通常也不设置
@param int flags:保留字段
|
7> av_read_frame:读取一帧数据
1 2 3
| @param AVFormatContext **ps
@param AVPacket *pkt:不能传null,需要预分配空间作为输出参数
|
8> av_seek_frame:移动到索引的frame
1 2 3 4 5 6 7
| @param AVFormatContext **ps
@param stream_index:索引,-1表示default,通常使用视频来做seek,使用音频seek有可能移到视频某个非关键帧的位置
@int64_t timestamp:移动到位置的时间戳
@int flags:标志位 AV_SEEKFLAG_BACKWARD[1]:往后找 AV_SEEKFLAG_FRAME[8]:只跳到关键帧
|
9> avformat_close_input:关闭打开的音视频文件
10> av_strerror:失败时存放错误信息
FFmpeg API调用部分
根据 url ,获取它的信息头,用这些信息来初始化解封装器及输入流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| formatContext = avformat_alloc_context();
AVDictionary *dictionary = nullptr; av_dict_set(&dictionary, "timeout", "5000000", 0);
int res = avformat_open_input(&formatContext, data_source, nullptr, &dictionary);
av_dict_free(&dictionary);
if(res){ callBackErrorEvent(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL, 0, av_err2str(res)); return; }
LOGI("avformat_open_input...");
|
读取一些媒体数据,通过读取的这些媒体数据,尝试分析流信息。尤其是对于没有信息头的媒体,例如MPEG等,这步就很重要了,是对第一步的补充。
1 2 3 4 5 6 7 8 9 10 11 12
| res = avformat_find_stream_info(formatContext, nullptr);
if(res < 0){ callBackErrorEvent(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS, 0, av_err2str(res)); return; }
this->duration = formatContext->duration / (AV_TIME_BASE/1000);
LOGI("avformat_find_stream_info : duration = %d ", duration);
|
一个媒体文件通常包含视频数据、音频数据、字幕信息等,有的地方将这些不同的数据类别称为轨道,在FFmpeg中称为流stream。根据解封装器中已经获取的流信息,打开合适的解码器。
有几个流,就要打开几个解码器。例如:有音频流,要找到对应的音频解码器;有视频流,要找到对应的视频解码器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| for(int i=0;i<formatContext->nb_streams;i++){ AVStream *stream = formatContext->streams[i]; AVCodecParameters *parameters = stream->codecpar; AVCodec *codec = avcodec_find_decoder(parameters->codec_id); if(!codec){ callBackErrorEvent(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL, 0, "can't find decoder!"); return; } AVCodecContext *codecContext = avcodec_alloc_context3(codec); if(!codecContext){ callBackErrorEvent(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL, 0, "init AVCodecContext error!"); return; }
res = avcodec_parameters_to_context(codecContext, parameters); if(res < 0){ callBackErrorEvent(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL, 0, av_err2str(res)); return; }
res = avcodec_open2(codecContext, codec, nullptr); if(res){ callBackErrorEvent(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL, 0, av_err2str(res)); return; }
if(parameters->codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO){ audioChannel = new AudioChannel(i, codecContext, stream->time_base, jniCallbackHelper); } else if(parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO){ if(stream->disposition & AV_DISPOSITION_ATTACHED_PIC){ continue; } AVRational fps_rational = stream->avg_frame_rate; double fps = av_q2d(fps_rational); videoChannel = new VideoChannel(i, codecContext, stream->time_base, fps, jniCallbackHelper); videoChannel->setRenderCallback(renderCallback); }
}
if(!audioChannel && !videoChannel){ callBackErrorEvent(THREAD_CHILD, FFMPEG_NO_MEDIA, 0, "no audio or video!"); return; }
LOGI("prepared finish...");
if(jniCallbackHelper){ jniCallbackHelper->onPrepared(THREAD_CHILD); }
|
把 视频 音频 的压缩包(AVPacket *) 循环获取出来 加入到队列里面去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void JJHPlayer::_start() { AVPacket *packet = nullptr; while (isPlaying){ packet = av_packet_alloc(); int res = av_read_frame(formatContext, packet); if(!res){ if(videoChannel && videoChannel->stream_index == packet->stream_index){ videoChannel->packets.insert(packet); }else if(audioChannel && audioChannel->stream_index == packet->stream_index){ audioChannel->packets.insert(packet); } }else if(res == AVERROR_EOF){ if (videoChannel->packets.empty() && audioChannel->packets.empty()) { break; } }else{ break; } } BaseChannel::releaseAVPacket(&packet); stop(); }
|
源码地址:https://github.com/jiajunhui/ffmpeg-jjhplayer