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,可以是rtmprtsphttp等,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可以和JNIEnvregisterNatives函数结合起来,实现动态的函数替换。很显然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播放器核心数据结构体的类,作者是直接使用的ffmpegijkplayer-sample/src/main/jni/ffmpeg/ffplay.cffmpeg的文件的基础上的扩展,里面引用了很多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.hinclude

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.cinclude

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_playerijkplayer_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替代BA就代表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是建立在ffmpegffmpeg/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

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

可以看见执行了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;类型的pbAVIOContext 结构体的分析放在第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 byteread MIME type (always ISO-8859)read file nameread 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_picff_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.cread_thread,作者在寻找h246流,即视频流

 

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

 

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

接着就是 open the streams

 

stream_component_open 函数在同一个文件里

 

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

 

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

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

现在audiovideo都有了,就准备播放了,主要包括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.cinit_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),没错,会调用AVInputFormatread_header()方法读取媒体文件的文件头并且完成相关的初始化工作。flv格式来看,会调用flv里实现的flv_read_header方法,在函数里没有找到代码,但是我感觉如果读取头文件成功应该会继续调用AVInputFormatread_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_initffmpeg/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_initff_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_bufferingstep_to_next_frame_lstream_update_pause_l等 。

代码中多次出现continue_read_thread


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

总结一下,循环读取数据部分:主要包括pauseresume操作处理、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循环):主要包括pauseresume操作处理、读取frame处理、AVFILTER处理、然后是将frame写入视频picture队列中以及每次解码后的清理动作。

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

avfilter_graph_free(&graph);

            graph = avfilter_graph_alloc();

添加framebuffer缓存区av_buffersrc_add_frame

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

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

queue_picture函数里,调用里YUV

SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame)

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

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

这里要总结两个结构体:

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

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

 

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

猜你喜欢

转载自bernoulli.iteye.com/blog/2328641