看了很久的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。