ijkplayer阅读学习笔记之从代码上看播放流程

看了很久的ijkplayer的视频播放,其实还是没有怎么看懂,只是个人浅浅的笔记

关键部分就是联网获取数据那部分,还没有搞定其实

从用户点击一个已有地址的网络视频开始,从源码分析播放流程。

1.        // init player  加载native底层库

IjkMediaPlayer.loadLibrariesOnce(null);

IjkMediaPlayer.native_profileBegin("libijkplayer.so");

第一句话是加载三个重要的so文件

libLoader.loadLibrary("ijkffmpeg");

libLoader.loadLibrary("ijksdl");

libLoader.loadLibrary("ijkplayer");

第二句话调用了一个native方法public static native void native_profileBegin(String libName);

2.  设置uri,可以是rtmp,rtsp,http等,native ffplay代码中会根据该uri匹配不同的流媒体协议,具体参考ffplay。 支持本地和网络视频。

if (mVideoPath != null)

mVideoView.setVideoPath(mVideoPath);

else if (mVideoUri != null)

mVideoView.setVideoURI(mVideoUri);

3. 分析网络视频源:接着openVideo()

先创建播放器 mMediaPlayer = createPlayer(mSettings.getPlayer());接着是播放器的相关设置

诸如:       mMediaPlayer.setOnPreparedListener(mPreparedListener);

mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);

mMediaPlayer.setOnCompletionListener(mCompletionListener);

mMediaPlayer.setOnErrorListener(mErrorListener);

mMediaPlayer.setOnInfoListener(mInfoListener);

mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);等

当然,还需要设置播放源,会调用

tv.danmaku.ijk.media.player.IMediaPlayer#setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String,java.lang.String>)

开始播放mVideoView.start();调用tv.danmaku.ijk.media.player.IMediaPlayer#start,这里java层就结束了,进入c底层,其实从加载native底层库开始就进入c底层了

4. 分析c底层:/ijkmedia/ijkplayer/android/ijkplayer_jni.c这个文件里 static JNINativeMethod g_methods[] 一个数组,声明了全部的java层的native方法 ,而调用native的方法集中在类 tv.danmaku.ijk.media.player.IjkMediaPlayer

5. 没想到居然是c主动调用java,通过反射生成java端所需要的函数,这个地方不是我们正常JNI的调用思路

正常调用JNI的思路:是通过javah,获取一组带签名函数,然后实现这些函数。这种方法很常用,也是官方推荐的方法。

分析一下JNI运行时机制:

当在系统中调用System.loadLibrary函数时,该函数会找到对应的动态库,然后首先试图找到"JNI_OnLoad"函数,如果该函数存在,则调用它。

JNI_OnLoad可以和JNIEnv的registerNatives函数结合起来,实现动态的函数替换。很显然ijk的作者是使用的这种调用方式

在 ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/android/ijkplayer_jni.c

然后在JNI_OnLoad 初始化好多内容,比如上图三个 global 型的函数

这样就可以理解作者为什么一开始就先调用三个 so 文件了

ijkplayer-sample/src/main/jni/ijkmedia/ijksdl/android/ijksdl_android_jni.c里也有JNI_OnLoad。

Ffmpeg的个人感觉是不需要,因为作者直接使用的ffmpeg的代码,只需要正常引入就好。

6. J4a里生成的两个java文件也是很重要的类

目前还不清楚作用

7. ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ff_ffplay_def.h  这个定义了ijk播放器核心数据结构体的类,作者是直接使用的ffmpeg的ijkplayer-sample/src/main/jni/ffmpeg/ffplay.c在ffmpeg的文件的基础上的扩展,里面引用了很多ffmpeg的源文件。基本上就不需要关心ffmpeg的源代码了。

8. 这里只分析libijkplayer.so

8.1  ijkmp_global_init 在 ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkplayer.c

void ijkmp_global_init()

{

ffp_global_init();

}

ffp_global_init() 在

ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkplayer_internal.h的include的

ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ff_ffplay.h里,具体定义代码在

ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ff_ffplay.c中,主要是ffmpeg的初始化工作。

8.2 ijkmp_global_set_inject_callback(inject_callback)是在

ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkplayer.c的include的

ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkavformat/ijkavformat.h里声明,

在ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkavformat/utils.c里定义

这里使用最多的是ijkavformat.h里的

typedef int (*IjkAVInjectCallback)(void *opaque, int message, void *data, size_t data_size);

这个声明引入了 IjkAVInjectCallback 类型作为函数指针的同义字,该函数有四个不同 类型的参数以及一个 int 类型的返回值

IjkAVInjectCallback ijkav_register_inject_callback(IjkAVInjectCallback callback);定义一个函数ijkav_register_inject_callback,这个函数的参数是一个函数指针,返回值也是个函数指针,相当于int ( *ijkav_register_inject_callback( int (*callback)(void *opaque, int message, void *data, size_t data_size) ) )(void *opaque, int message, void *data, size_t data_size);

由于int (*callback)(void *opaque, int message, void *data, size_t data_size)

是IjkAVInjectCallback ,上面的简化一下就是

int ( *ijkav_register_inject_callback( IjkAVInjectCallback ) )(void *opaque, int message, void *data, size_t data_size);这里的

*ijkav_register_inject_callback( IjkAVInjectCallback ) 又是一个返回IjkAVInjectCallback的函数 ,再简化一下,即

int IjkAVInjectCallback (void *opaque, int message, void *data, size_t data_size);

8.3 FFmpegApi_global_init(env)在

ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/android/ffmpeg_api_jni.c

主要作用是 初始化 tv.danmaku.ijk.media.player.ffmpeg.FFmpegApi#av_base64_encode 的函数

9. 在System.loadLibrary 三个so文件时作者做了一些初始化的工作,接着调用native native_profileBegin方法,在

ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/android/ijkplayer_jni.c的

IjkMediaPlayer_native_profileBegin 方法,卡在void monstartup(const char *libname);这个函数,没有找到定义的地方。决定试试注释IjkMediaPlayer.native_profileBegin("libijkplayer.so");

tv/danmaku/ijk/media/sample/activities/VideoActivity.java:139

这句话不执行,注释后依旧可以播放,我就不能理解了。哈哈,此处留坑。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

10. 接着看看_setDataSource(String path, String[] keys, String[] values)别看有三个参数,实际调用时只需要一个参数

tv.danmaku.ijk.media.player.IjkMediaPlayer#383     _setDataSource(path, null, null);

关键就是 ijkmp_set_data_source(mp, c_path);这是通过include "ijkplayer_android.h"然后#include "../ijkplayer.h",具体定义在ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkplayer.c

ijkmp_set_data_source(IjkMediaPlayer *mp, const char *url)两个参数,一个参数是是IjkMediaPlayer,

typedef struct IjkMediaPlayer IjkMediaPlayer;

(ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkplayer.h)

只是声明了名字,定义在ijkplayer-sample/src/main/jni/ijkmedia/ijkplayer/ijkplayer_internal.h

另一个参数是视频源地址。

这是视频源 mp->data_source = strdup(url);这样IjkMediaPlayer类型的mp 里的VideoState (ff_ffplay_def.h)类型的 is 里的filename 就有了视频源

接着就执行到 ijkmp_change_state_l(mp, MP_STATE_INITIALIZED);更换播放器状态

void ijkmp_change_state_l(IjkMediaPlayer *mp, int new_state)

{

mp->mp_state = new_state;

ffp_notify_msg1(mp->ffplayer, FFP_MSG_PLAYBACK_STATE_CHANGED);

}

ffp_notify_msg1这个函数是通过#include "ijkplayer_internal.h"再#include "ff_ffplay.h"再#include "ff_ffplay_def.h"进来,定义为

inline static void ffp_notify_msg1(FFPlayer *ffp, int what) {

msg_queue_put_simple3(&ffp->msg_queue, what, 0, 0);

}

其中 inline这个关键词会给编译器这样一种建议:不要进行实际的函数调用而只是将这段代码插入到进行调用的地方。

上面用到的一个比较重要的结构体FFPlayer 定义在ijkmedia/ijkplayer/ff_ffplay_def.h#494

目前还不知道其具体作用,但是感觉这是播放时一个很重要的结构体。

msg_queue_put_simple3在 ijkmedia/ijkplayer/ff_ffmsg_queue.h

继续跟踪代码,发现走到msg_queue_put_private这个函数,将其放在消息队列里。

至于放在哪个消息队列里,谁又负责读取消息队列里的消息,这个在后面分析。

按照递归的思想,依次返回,

在ijkplayer_jni.c里 IJK_CHECK_MPRET_GOTO(retval, env, LABEL_RETURN); 这个不知是干嘛的,只是看见difine里,继续留坑。。。。。。。。。。。。。。。。。。。。。。。。。。。。

11. 现在开始执行tv.danmaku.ijk.media.player.IjkMediaPlayer#_start的方法,

在ijkmedia/ijkplayer/android/ijkplayer_jni.c#226  ijkmp_start(mp);根据作者的命名规则,ijkmp...都是在ijkmedia/ijkplayer/ijkplayer.h里,这个函数定义在197行

实现自然就在ijkplayer.c#441

最重要的应该就是这句话了 ffp_notify_msg1(mp->ffplayer, FFP_REQ_START);

ffp_notify_msg1 通过 #include "ijkplayer_internal.h",#include "ff_ffplay.h",#include "ff_ffplay_def.h",调用了 msg_queue_put_simple3(&ffp->msg_queue, what, 0, 0);这个跟第10条是调用一个函数,都是msg_queue_put_simple3,这次传递的消息是FFP_REQ_START,定义在ijkmedia/ijkplayer/ff_ffmsg.h里,关于消息队列的分析,在后面分析。

12. 其实在这里会有个疑问,作者先是设置了视频源,接着开始播放,这是两个不同的C函数,都使用了 IjkMediaPlayer 这个对象,用面向对象的语言来提问就是:如何保证这两个对象是同一个对象,即设置播放源的IjkMediaPlayer和播放的IjkMediaPlayer是同一个对象

设置播放源里:IjkMediaPlayer *mp = jni_get_media_player(env, thiz);

播放函数里面:IjkMediaPlayer *mp = jni_get_media_player(env, thiz); (JNIEnv *env, jobject thiz)

jni_get_media_player在ijkplayer_jni.c#61 ,调用了IjkMediaPlayer *mp = (IjkMediaPlayer *) (intptr_t) J4AC_IjkMediaPlayer__mNativeMediaPlayer__get__catchAll(env, thiz);

这里用到的就是上文第六点提到j4a,在

ijkmedia/ijkj4a/j4a/class/tv/danmaku/ijk/media/player/IjkMediaPlayer.h#53  define

#define A B 的意思就是在程序里使用A替代B,A就代表B,

看看作者是怎么实现的

在这里使用了一个JNI函数

GetLongField()      NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);

Static 修饰的fieldID即可以保证两次获取的是同一个对象。

上面的方法里有个强转 ,intptr_t类型是为指针准备的,C语言指针用来保存变量或常量的地址,地址由处理器的位数决定,intptr_t在不同的平台是不一样的,始终与地址位数相同,因此用来存放地址,即地址。

13. 分析消息队列:现在知道了c里面关键变量都是做了单例的,比如我们现在要分析的IjkMediaPlayer。

其定义在ijkmedia/ijkplayer/ijkplayer_internal.h

标红的,一个是线程互斥锁pthread_mutex_t,这个的使用在源码中随处可见,另一个是很重要的FFPlayer,定义在ijkmedia/ijkplayer/ff_ffplay_def.h里,结构体里面包含了很多内容,目前只分析一个MessageQueue  msg_queue;定义在ijkmedia/ijkplayer/ff_ffmsg_queue.h

msg_queue_put_private一大段处理消息的算法,找出关键函数 SDL_CondSignal(q->cond);通过#include "ff_ffinc.h",#include "ijksdl/ijksdl.h",#include "ijksdl_mutex.h"。具体定义在ijkmedia/ijksdl/ijksdl_mutex.c,

pthread_cond_signal(pthread_cond_t *cond)函数是用来在条件满足时,给正在等待的对象发送信息,表示唤醒该变量,一般和pthread_cond_wait函数联合使用

其实到这个地方,就会发现源代码分析不下去了,因为找不到可用函数了,这里用来唤醒线程,但是又不清楚唤醒的是哪个进程,这个进程什么时候堵塞的,其实是我们少分析了一部分,作者在最开始加载三个so文件时,做了很多初始化的工作,这里就不可避免要去分析ffmpeg的源代码了。

回过头来再看看初始化三个so文件时的操作

av_ 开头的函数都是ffmpeg里的,avformat_network_init在ffmpeg/libavformat/avformat.h里声明,做网络的初始化,可惜我还没有找到定义的位置。这里关注ijkav_register_all,声明在 ijkmedia/ijkplayer/ijkavformat/ijkavformat.h ,定义在ijkavformat/allformats.c

标红的部分是我要重点分析的地方

URLProtocol ffmpeg里一个很重要的结构体,定义在libavformat/url.h

URLProtocol存储输入视音频使用的封装格式,功能就是完成各种输入协议的读写等操作

其中包含了用于协议读写的函数指针。例如:

url_open():打开协议。

url_read():读数据。

url_write():写数据。

url_close():关闭协议。

但输入协议种类繁多,它是怎样做到“大一统”的呢?

原来,每个具体的输入协议都有自己对应的URLProtocol。

比如ijktcphook协议

ijklongurl:

还有好多,不一一例举了,它们把各自的函数指针都赋值给了URLProtocol结构体的函数指针。

上面的代码中,有个很关键的关键字,extern,使用时extern URLProtocol ijkff_##x##_protocol;

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

上面的声明最终都是调用ffurl_register_protocol这个ffmpeg里的函数,无从下手,看来得需要阅读ffmpeg的源码

原谅我源码读的有够失败,没有找到ffurl_register_protocol的声明和定义。。。。。。。

但是,从这个同名大概可以猜测一些,应该就是引用这些文件吧,这只是猜测而已。

14. 到这里好像已经进行不下去了,因为全部都是开始调用ffmpeg里的代码了,顺序走不下去可以逆序嘛。现在就逆序分析,我们知道ijk的作者写的ijkmedia/ijkplayer/ff_ffplay.c是建立在ffmpeg的ffmpeg/ffplay.c的基础上的,基本上这里面的代码是直接使用ffmpeg的代码,在这个文件里有一个read_thread函数,ffmpeg的作者写了一个注释

Ijk的作者应该是重写了ffplay.c,从read_thread的被调用来看,ffp_prepare_async_l --> stream_open --> read_thread,我没有找到ffp_prepare_async_l 被调用的地方,然后看到read_thread里面的代码

很熟悉有没有,输出的日志嘛

可以猜测这段代码是在设置视频源初始化时被调用的,不知是否还记得分析的第10条,而且看参数ffp_prepare_async_l(FFPlayer *ffp, const char *file_name) ,一个是FFPlayer ,一个是file_name,如果是网络视频,那就是视频源地址了。

虽然还没有分析代码里线程池睡眠唤醒的机制,但是可以猜测使用的是同一个一个FFPlayer 对象。

继续分析代码,在stream_open 函数里,不是我瞎猜想,看ijk作者注释

证明我的猜想分析是正确的,然后就到了我们很重要的read_thread。

这个函数主要就是读取二进制流,里面用到了以下这些很重要的结构体:FFPlayer,VideoState,AVFormatContext,AVPacket,

可以看见执行了avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);

这个函数是ffmpeg里的,应该是读取视频文件。这个函数的声明int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options); 在ffmpeg/libavformat/avformat.h

定义在ffmpeg/libavformat/utils.c ,知道这几个参数很重要,但是还清楚其具体作用,那我怎么阅读呢,int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options),看参数,有个filename,那我就已filename为主线,第一次被调用是init_input(s, filename, &tmp),这个函数在utils.c,好像是一个什么评分规则,没看懂反正,估计是判断视频源是什么格式的。我不是瞎猜测的哈

Ffmpeg的作者对这个函数有注释,它的主要工作就是打开输入的视频数据并且探测视频的格式。这里没有几行代码,但是好多return,足见次函数的算法逻辑。

调用av_probe_input_buffer2这个内容很多,此处不分析,在第16条分析

这里有个目测是最基础的结构体 AVFormatContext ,定义在 ffmpeg/libavformat/avformat.h

作用就是 Format I/O context 中文讲就是处理封装格式(FLV/RMVB/MP4等),你要是好奇ffmpeg到底支持哪些格式,我告诉你个地方,ffmpeg/libavformat/allformats.c,里面有av_register_all,熟悉吧,初始化时都执行的函数,里面出现的格式都是ffmpeg支持的。

这个结构体里有两个指针 protocol_whitelist 和 protocol_blacklist ,大概猜测就是白名单黑名单的作用,这个出现应该是处理上文第13条里种类繁多的URLProtocol。

接着执行到

/* e.g. AVFMT_NOFILE formats will not have a AVIOContext */

if (s->pb)

ff_id3v2_read(s, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta, 0);

这里有个

AVIOContext *pb;类型的pb。AVIOContext 结构体的分析放在第15条,这里不分析。

ff_id3v2_read的定义在ffmpeg/libavformat/id3v2.c。目测是处理id3v2头信息的。这个函数调用id3v2_read_internal,里面有个函数avio_tell定义在ffmpeg/libavformat/avio.h,

接着执行 avio_read。声明在ffmpeg/libavformat/avio.h

原谅我没有找到这个函数的定义,avio_read应该是联网读取是调用。联网的分析放在第16条。

读取网络数据包头信息时调用id3v2_parse(pb, metadata, s, len, buf[3], buf[5], extra_meta);接着调用get_extra_meta_func,会使用到id3v2_extra_meta_funcs[]这个数组,里面有read_geobtag这样一个方法,这个方法里会read encoding type byte,read MIME type (always ISO-8859),read file name,read content description 。

别小看这里的读取头信息,这里其实ff_id3v2_read的操作结果是直接对参数 &id3v2_extra_meta 进行更改的。获取到头信息后会执行ff_id3v2_parse_apic,这个函数也是在id3v2.c 里,这个函数带给我们有一个ffmpeg里很重要的结构体AVStream。定义在ffmpeg/libavformat/avformat.h。视音频流对应的结构体,用于视音频编解码,

同时AVStream这个结构体里包含了另外一个很重要的结构体,AVPacket attached_pic,这个attached_pic在ff_id3v2_parse_apic使用率较高

AVPacket 的定义在ffmpeg/libavcodec/avcodec.h ,主要是存储压缩数据(视频对应H.264等码流数据,音频对应AAC/MP3等码流数据),这个结构真的很重要,ffmpeg的原作者注释

For video, it should typically contain one compressed frame. For audio it maycontain several compressed frames. 即视频和音频还不一样。

avformat_open_input这个函数同样的,执行完成后对参数AVFormatContext **ps进行了更新。

在ffmpeg/libavformat/utils.c里发现一个函数avformat_find_stream_info,虽然还没有发现其使用的地方,但是大概看了一下函数方法,应该是解析流,可以解析出很多信息,没有细看内容,应该是可以解析出视频流的详细信息,时长,比特率等等。

退回到ff_ffplay.c的read_thread,作者在寻找h246流,即视频流

我们知道访问网络是获取的数据包有很多,作者取出其中的H264包。

av_find_best_stream的声明在 ffmpeg/libavformat/avformat.h,定义在ffmpeg/libavformat/utils.c,ffmpeg的作者有一套算法找出用户最想要的streams,我没有去研究去算法。

接着就是 open the streams

stream_component_open 函数在同一个文件里

其实这个函数的最主要的作用应该是创建audio_thread和video_thread线程,关于这两个线程,放在18条分析

在decoder_start 里 调用packet_queue_start(d->queue);

当然前面调用了avcodec_find_decoder,根据id找到解码器。

现在audio和video都有了,就准备播放了,主要包括SDL_mutex信号量创建(这个就是类似13条里说的线程锁

)、AVFormatContext创建、打开输入文件、解析码流信息、查找音视频数据流并打开对应的数据流,这是函数里的第一部分。

接着后面跟了一个死循环,开始循环读取视频流了

循环读取代码里好多的算法逻辑,好些地方不太明白。具体分析放在17条,这是第二部分。

代码里还有一部分fail时的操作,在代码的最后,主要包括退出前的等待、关闭音视频流、关闭avformat、给主线程发送FF_QUIT_EVENT消息以及销毁SDL_mutex信号量,这是第三部分。

15. AVIOContext 是ffmpeg中很重要的结构体,定义在libavformat/avio.h,输入输出对应的结构体,用于输入输出(读写文件,RTMP协议等),ffmpeg的作者给了一个形象的图来解释其重要性

16. 分析ffmpeg的获取数据(联网)

av_probe_input_buffer2,调用在ffmpeg/libavformat/utils.c的init_input函数

声明在ffmpeg/libavformat/avformat.h,作用Probe a bytestream to determine the input format.定义在 ffmpeg/libavformat/format.c

使用了一个结构体AVProbeData 定义在libavformat/avformat.h,用于存储输入文件的一些信息

av_probe_input_buffer2也可以分为三部分分析,初始化,循环读取,反初始化,重点分析循环读取部分,循环部分调用avio_read()读取数据,调用av_probe_input_format2推测文件格式

int avio_read(AVIOContext *s, unsigned char *buf, int size); Read size bytes from AVIOContext into buf.

av_probe_input_format2定义在ffmpeg/libavformat/format.c ,里面调用av_probe_input_format3,这个也是定义在ffmpeg/libavformat/format.c,这个函数的返回值是AVInputFormat类型的,定义在ffmpeg/libavformat/avformat.h,里面定义了一个int (*read_probe)(AVProbeData *) 这个是av_probe_input_format3函数里循环部分调用的比较重要的,关键read_probe 是一个函数呀,这个函数用于获得匹配函数的函数指针,不同的封装格式包含不同的实现函数。以常见的flv格式来说,在ffmpeg/libavformat/flvdec.c

read_probe对应的就是 live_flv_probe,live_flv_probe定义在同一个文件里。在比如swf格式

每个格式都有不同的匹配规则,为了判断当前文件的格式,就已得分的高低来判断,av_probe_input_format2的操作结果是更新参数里的得分,同时得出文件是什么格式的。

现在可以回到init_input函数,在avformat_open_input里调用结束后,接着执行到ff_id3v2_read函数,这个在14条里已分析,接着执行s->iformat->read_header(s),没错,会调用AVInputFormat的read_header()方法读取媒体文件的文件头并且完成相关的初始化工作。以flv格式来看,会调用flv里实现的flv_read_header方法,在函数里没有找到代码,但是我感觉如果读取头文件成功应该会继续调用AVInputFormat的read_packet()读取网络数据包,原谅我没有找到。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

虽然我没有找到,但代码就是这么执行的,可以接着分析,

回到avformat_open_input,接着调用avformat_queue_attached_pictures,这里面有个方法,add_to_pktbuf,作用是 Add the packet in the buffered packet list。完成后,紧接着就是update_stream_avctx,有新的packet到达,里面做的操作无非就是判断当前状态,然后清空队列等等。

由于正向没有找到入口函数,这里才有逆向分析

主要是分析两个文件 ffmpeg/libavformat/url.c 和 ffmpeg/libavformat/network.c 从源码中分析出ffmpeg是使用socket联网

不知是否还记得13条里初始化时有个网络的输出化avformat_network_init,这个函数的定义在ffmpeg/libavformat/utils.c,这里有两个初试化。一个ff_network_init,一个ff_tls_init。

ff_network_init在ffmpeg/libavformat/network.c里定义,里面用了一个全局变量ff_network_inited_globally,在 .h 文件里声明了extern int ff_network_inited_globally;还没有找到其定义哈,哎呀都什么眼神,在avformat_network_init函数里,就调用了ff_network_inited_globally = 1;

ff_tls_init定义也在network.c,会用到两个函数ff_openssl_init和ff_gnutls_init,都是声明在ffmpeg/libavformat/tls.h,至于定义嘛,我还没有找见。只好看看注释咯

17. 循环读取分析:即for死循环里的代码

先是对是否暂停(暂停就不接受数据包了

,看来这个网络接收这块是一直连接着,只要没停止就一直接收着数据),是否拖动快进或快退判断接着往packet的队列里put

还有判断队列是否满了 if the queue are full, no need to read more

av_read_frame 声明在 ffmpeg/libavformat/avformat.h ,作用:Return the next frame of a stream.

作者的算法逻辑很强,当然这段代码里只是往packet队列里put,只是取出packet,在取的时候要判断当前的各种状态,暂停?快进?队列是否满,正在播放?等等,这些函数都是处理这些逻辑的,比如:ffp_toggle_buffering,step_to_next_frame_l,stream_update_pause_l等 。

代码中多次出现continue_read_thread,

从其名字上来看,是一个控制read_thread线程是否继续阻塞的信号量,上面两次阻塞的地方分别是:packet队列已满,需要等待一会(即超时10ms)或者收到信号重新循环;读数据失败,av_read_frame出错时,如读取网络实时数据时取不到数据,此时也需要等待或者收到信号重新循环。

总结一下,循环读取数据部分:主要包括pause和resume操作处理、seek操作处理、packet队列写入失败处理、读数据结束处理、然后是读数据并写入到对应的音视频队列中。

处理真正播放不在这里,继续分析

18. audio_thread和video_thread

定义在ff_ffplay.c,会发现作者开启线程是把其设为异步线程

ffplay_video_thread是处理视频核心实现部分,这个函数同read_thread一样也是分了三部分

初始化部分:主要包括AVFrame创建和AVFilterGraph创建。

AVFrame *frame = av_frame_alloc();

AVFilterGraph *graph = avfilter_graph_alloc();

循环解码部分(for循环):主要包括pause和resume操作处理、读取frame处理、AVFILTER处理、然后是将frame写入视频picture队列中以及每次解码后的清理动作。

读取get_video_frame,作者每次都是先释放再初始化AVFilterGraph 。

avfilter_graph_free(&graph);

graph = avfilter_graph_alloc();

添加frame到buffer缓存区av_buffersrc_add_frame,

其中AVFILTER部分,过滤器嘛,学过通信原理都知道,接收到的信号会有损失,有杂波,需要对解码后的数据进行滤波,去除方块效应

出现了一个结构体,AVRational,没有找到其定义,但是从使用来看,这个结构体里面有两个参数:frame_rate.num && frame_rate.den,应该就是用这两个参数算出一个值,类似分式那种分子分母。

queue_picture函数里,调用里YUV

SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame)

这里使用到了YUV,大概流程就是ffmpeg解码,转成YUV格式的视频帧,然后再使用sdl的yuv覆盖的模式进行渲染,剩下的就是显示了,这个是SDL处理的,用于视频图像显示和刷新,暂时不做分析。

结束部分:主要包括刷新codec中的数据、释放AVFilterGraph、释放AVPacket以及释放AVFrame。

这里要总结两个结构体:

AVFrame:存储非压缩的数据(视频对应RGB/YUV像素数据,音频对应PCM采样数据)

AVPacket:存储压缩数据(视频对应H.264等码流数据,音频对应AAC/MP3等码流数据)

audio_thread的分析类似,只是少了YUV的渲染,不做重复分析了,源码在ff_ffplay.c。

时间: 2024-10-04 16:29:32

ijkplayer阅读学习笔记之从代码上看播放流程的相关文章

python 学习笔记 3 -- 数据结构篇上

数据结构是可以处理一些 数据 的 结构 .或者说,它们是用来存储一组相关数据的.在Python中有三种内建的数据结构--列表.元组和字典.本文主要对这三种数据类型以及相关的使用做介绍,以例子的形式演示更加容易理解! 1.列表(List) 列表是处理一组有序项目的数据结构,即你可以在一个列表中存储一个 序列 的项目.在Python中,你在每个项目之间用逗号分割. 列表中的项目应该包括在**方括号**中,这样Python就知道你是在指明一个列表.一旦你创建了一个列表,你可以添加.删除或是搜索列表中的

Maven学习笔记之——仓库(上)

Maven学习笔记之--仓库(上) 1.    何为maven仓库 Maven可以在某一指定位置统一存放所有maven项目共享的构件.此指定位置就是maven仓库.实际的项目将不再自己存放其所依赖的构件.他们只需要声明这些依赖的坐标.在需要的时候就会自动根据坐标找到仓库中的构件.并使用他们. 仓库的意义:减少磁盘占用空间.去除大量重复的构件.尤其是项目越来越多.越来越大的时候.更便于统一管理所有控件. 2.    仓库的布局 任何一个构件都有其唯一的坐标.根据这个坐标可以定义其在仓库中的唯一存储

树莓派学习笔记——定时向yeelink上传树莓派CPU温度

0 前言 本文通过python文件IO操作获得树莓派CPU温度信息,通过python request库周期性向yeelink平台上传温度,修改rc.local脚本使得该python脚本在开机时便在后台运行,向平台每5分钟上报一次温度信息. 网上查阅了很多关于linux开机启动的博文,尝试了几遍居然发现方法都无法实现开机启动效果.我想可能是操作系统或开发环境的微小差异产生的,如果发现博文中的内容存在问题,请及时留言,我查证之后定当修改. [相关博文] [树莓派学习笔记--获取树莓派CPU温度] [

Citrix XenMobile学习笔记之六:XenMoble业务访问数据流程

总体访问流程图 终端设备注册流程 Android设备注册流程 到google Play或亚马逊应用商店或者豌豆荚.Citrix官网,下载思杰Worx Home应用.并在设备上安装. 当系统提示您安装该应用程序,单击下一步,然后单击安装. 安装Worx Home之后,点击启动. 输入您的认证信息,如设备管理器服务器名,用户主体名称(UPN),或电子邮件地址的名称,然后单击下一步. 在激活设备管理员屏幕上,点击激活. 输入您的账户密码,然后点击点登录. 根据XenMobile的配置方式,您可能会被要

windows下《七天学会NodeJS》学习笔记之二--代码的组织和部署

本系列第一篇:<windows下<七天学会NodeJS>学习笔记之一--NodeJS基础>,请参见这儿:http://blog.csdn.net/fm2005/article/details/41348813 模块路径解析规则:nodejs支持三种解析方式:/或C:开头的绝对路径:./开头的绝对路径:按照一定规则解析路径,直到找到模块位置. 内置模块:如果传递给require的是NodeJS内置模块名称,则不解析,直接返回内部模块导出对象. node_modules目录:node_

[原创]java WEB学习笔记50:文件上传案例

本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱好者,互联网技术发烧友 微博:伊直都在0221 QQ:951226918 ---------------------------------

【Linux 学习笔记】戏说Git (上)

本文为<廖雪峰Git教程>学习笔记,原地址: http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000 萌新入门,略有心得,戏说一番. 本章包括两部分内容, 1. git 本地库中文件的添加和提交: 2. 连接github 添加远程库 先来个正版概念镇个贴: Git是目前世界上最先进的分布式版本控制系统(没有之一). 假设, 我是一个单机游戏爱好者(好吧这不是假设..) 那么Git 就可

javascript高级程序设计 学习笔记 第五章 上

第五章 引用类型的值(对象)是引用类型的一个实例.在 ECMAScript 中,引用类型是一种数据结构, 用于将数据和功能组织在一起.它也常被称为类,但这种称呼并不妥当.尽管 ECMAScript 从技术上讲是一门面向对象的语言,但它不具备传统的面向对象语言所支持的类和接口等基本结构.引用类型有时候也被称为对象定义,因为它们描述的是一类对象所具有的属性和方法. 对象是某个特定引用类型的实例.新对象是使用 new 操作符后跟一个构造函数来创建的. 构造函数本身就是一个函数,只不过该函数是出于创建新

Java学习笔记四(代码块 )

1 代码块的分类:java中的代码块是指使用{}括起来的一段代码,根据位置不同可以分为四种: 普通代码块 构造快 静态代码块 同步代码块 今天主要学习前三种代码块,同步代码块在学习到多线程部分的时候再加学习. 2 普通代码块:直接定义在方法中的代码块,如下: public class CodeSpace { public static void main(String args[]){ { int x = 30; System.out.println("普通代码块x="+x); } /