FFmpeg之解封装

FFmpeg的解封装大概可分为以下九个步骤:

  1. 打开媒体地址(文件路径、视频URL)
  2. 查找媒体中的音视频流信息
  3. 根据流信息流个数循环查找
  4. 获取媒体流
  5. 从流获取编解码参数
  6. 根据参数获取编解码器
  7. 创建编解码器上下文
  8. 使用上面的参数初始化编解码器上下文
  9. 打开解码器
  10. 循环读取压缩数据AVPacket

回忆下上篇文章的图

ffmpeg解封装解码流程API概况

主要代码

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);
}
});
}
//...

//Native方法函数定义
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);

/**
* 参数1,AVFormatContext *
* 参数2,路径
* 参数3,AVInputFormat *fmt Mac、Windows 摄像头、麦克风, 目前安卓用不到
* 参数4,各种设置:例如:Http 连接超时, 打开rtmp的超时 AVDictionary **options
*/
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;
}

//Audio部分
if(parameters->codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO){
audioChannel = new AudioChannel(i, codecContext, stream->time_base, jniCallbackHelper);
}
//Video部分
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){ // end of file == 读到文件末尾了 == AVERROR_EOF
// 表示读完了,要考虑释放播放完成,表示读完了 并不代表播放完毕
if (videoChannel->packets.empty() && audioChannel->packets.empty()) {
break; // 队列的数据被音频 视频 全部播放完毕了,我在退出
}
}else{
break;//出现错误,结束当前循环
}
}
BaseChannel::releaseAVPacket(&packet);
stop();
}

源码地址:https://github.com/jiajunhui/ffmpeg-jjhplayer