ffplay如何支持多协议

来源:互联网 发布:网络摄像头用户名密码 编辑:程序博客网 时间:2024/06/08 02:26

分析过一个简化版的,现在开始搞新版本ffmpeg-3.0.2。ffmpeg的代码量太大了,必须得找准点,并且不能急躁,弄不懂的先放着,后面一步一步的解决。

插一个流程图的例子,这个例子足够可以画流程图啦.用markdown来画流程图,就简单了.平常一个流程图基本解决问题.

Created with Raphaël 2.1.0Startstream_openread_threadavformat_open_inputyes or noEndyesno

有一个网址有markdown的语法: https://www.zybuluo.com/mdeditor?url=https://www.zybuluo.com/static/editor/md-help.markdown

好,言归正传.

主要有两种测试,一种播放flv file文件,一个是播放rtmp。编解码是H264和AAC。

ffplay test.flv   //这个是来自于磁盘的flv fileffplay rtmp://127.0.0.1:1935/live/test  //这个是来自于网络rtmp协议

这一篇blog主要是分析ffplay如来应对下面三种逻辑的:

  1. 数据来源的不同,也是IO的不同,ffmpeg中叫protocol。譬如:file,rtmp.
  2. 数据的格式不同,也是封装的不同,ffmpeg中叫formt,譬如flv.
  3. 编解码的不同,ffmpeg中叫codec

针对上面三种结构体定义了很多对象.

1 如何确定IO和fmt

这两个的判断逻辑是揉在一块的,所以不得不在一起来分析.有一个重要函数:avformat_open_input ,在这个函数中进行了初始化,确定了proto(数据来源)和fmt(封装格式).

我添加了一些日志,分析一下播放test.flv的日志:

##########read_thread start          //解析线程启动##########start avformat_open_input  //准备初始化##########init_input start,pb:00000000,fmt:00000000  //初始化函数中,init_input最重要 ##########av_probe_input_format2 start //调用了av_probe_input_format3函数,开始探测fmt中,由于io没有开启,连Proto都没有定,探测没起到任何作用.##########io_open_default start  //执行io_open_default,这个是先就定义好了的############ffurl_alloc,URLProtocol:file   //在这里确定了URLProto,具体逻辑下面详细说.##########av_probe_input_buffer2 start     //通过读取数据来确定fmt##########av_probe_input_format2 start     //这是第二次探测,这一下有数据了##########av_probe_input_format3 start     //这里会遍历所有fmt...Probing flv score:100 size:2048    //通过长度为2048的buf,flv检测为100分##########av_probe_input_format3 extensions:flv  //并判断了后缀名....###############av_probe_input_buffer2 fmt:flv  //这里就已经确定了FMT.

总结一下为,先通过文件名来确定是file proto(磁盘文件),之后读取到数据,解析2048个字节,判断出的fmt为flv.

分析一下播放rtmp://…的日志:逻辑基本一样.

##########read_thread start##########start avformat_open_input##########init_input start,pb:00000000,fmt:00000000##########av_probe_input_format2 start##########av_probe_input_format3 start//第一次探测,没用....##########init_input start io_open##########io_open_default start############ffurl_alloc,URLProtocol:rtmp   //确定了RTMP##########ffurl_open_whitelist start############ffurl_alloc,URLProtocol:tcp    //又来一个有一个PROTO???[rtmp @ 00d9f400] Handshaking...     //在读取数据前,需要走rtmp的一些协议:握手,play命令的发送##########ffio_fdopen start                         //open##########init_input start2 av_probe_input_buffer2  //第二次探测##########av_probe_input_buffer2 start##########av_probe_input_format2 start##########av_probe_input_format3 start....##########av_probe_input_format3 traver fmt:flv,nodat:0Probing flv score:100 size:2048##########av_probe_input_format3 traver result,fmt:flv,score:100,max_score:100...###############av_probe_input_buffer2 fmt:flv

两个逻辑差不多,但是有两个震惊的是:

  1. rtmp的fmt想不到是FLV
  2. rtmp有两个PROTO,rmtp和tcp.相当于有两层

这两个逻辑要好好分析一下,留在下一篇吧.这一篇的重点不是如何获取数据,而是获取到数据后如来来判断.

有这么多PROTO,是通过链表来组织的,肯定在初始化中.

void av_register_all(void){    ...    REGISTER_PROTOCOL(FILE,             file); //file也是一种protocol.    REGISTER_PROTOCOL(RTMP,             rtmp);}//定义一个宏,所以要在search代码时,经常找不到,这就是写成了宏.对查询代码造成了一定的麻烦.#define REGISTER_PROTOCOL(X, x)                                         \    {                                                                   \        extern URLProtocol ff_##x##_protocol;                           \        if (CONFIG_##X##_PROTOCOL)                                      \            ffurl_register_protocol(&ff_##x##_protocol);                \    }//first_protocol就是一个链表的头,register proto就是插入到链表中.int ffurl_register_protocol(URLProtocol *protocol){    URLProtocol **p;    p = &first_protocol;    while (*p)        p = &(*p)->next;    *p             = protocol;    protocol->next = NULL;    return 0;}//列出几个重要函数,open,read,write,这是操作IO的函数typedef struct URLProtocol {    const char *name;    int     (*url_open)( URLContext *h, const char *url, int flags);    int     (*url_read)( URLContext *h, unsigned char *buf, int size);    int     (*url_write)(URLContext *h, const unsigned char *buf, int size);} URLProtocol;

fmt的初始化和Proto的类似.fmt又叫muxer和demuxer.用链表串起来,通过name来区别

void av_register_all(void){    REGISTER_MUXDEMUX(FLV,              flv);}#define REGISTER_MUXER(X, x)                                            \    {                                                                   \        extern AVOutputFormat ff_##x##_muxer;                           \        if (CONFIG_##X##_MUXER)                                         \            av_register_output_format(&ff_##x##_muxer);                 \    }#define REGISTER_DEMUXER(X, x)                                          \    {                                                                   \        extern AVInputFormat ff_##x##_demuxer;                          \        if (CONFIG_##X##_DEMUXER)                                       \            av_register_input_format(&ff_##x##_demuxer);                \    }#define REGISTER_MUXDEMUX(X, x) REGISTER_MUXER(X, x); REGISTER_DEMUXER(X, x)static const AVClass flv_class = {    .class_name = "flvdec",    .item_name  = av_default_item_name,    .option     = options,    .version    = LIBAVUTIL_VERSION_INT,};AVInputFormat ff_flv_demuxer = {    .name           = "flv",    .long_name      = NULL_IF_CONFIG_SMALL("FLV (Flash Video)"),    .priv_data_size = sizeof(FLVContext),    .read_probe     = flv_probe,    .read_header    = flv_read_header,    .read_packet    = flv_read_packet,    .read_seek      = flv_read_seek,    .read_close     = flv_read_close,    .extensions     = "flv",    .priv_class     = &flv_class,};

不同的URLProtocol就有了不同的读写方式,那重点就在于如何找到URLProtocol.

初始化完成后就得进入read,流程是:stream_open->read_thread->avformat_open_input->init_input,好了,到了init_input就要细看了,删掉了一些不关注的东西.

static int init_input(AVFormatContext *s, const char *filename,                      AVDictionary **options){    int ret;    AVProbeData pd = { filename, NULL, 0 };    int score = AVPROBE_SCORE_RETRY;    //av_probe_input_format2,这是第一次探测,io没打开,为什么要有这一步呢?    if ((        (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))        return score;    //所以必须得走到这里来io_open.    //io_open在avformat_open_input前面初始化 AVFormatContext *s 直接赋值的.    if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)        return ret;    //找到了Proto,并open,可以进行第二次探测.     //av_probe_input_buffer2确定了fmt           return av_probe_input_buffer2(s->pb, &s->iformat, filename,                                 s, 0, s->format_probesize);}

重点要分析一下 s->io_open .先从它的赋值开始.

// s = avformat_alloc_context(),在alloc时调用 avformat_get_context_defaults .static void avformat_get_context_defaults(AVFormatContext *s){    memset(s, 0, sizeof(AVFormatContext));    s->av_class = &av_format_context_class;    s->io_open  = io_open_default;  /////////就是它    s->io_close = io_close_default;    av_opt_set_defaults(s);}

那就得直接跳到 io_open_default. ffmpeg用到了很多函数赋值,用C语言来实现面向对象的搞法.
io_open_default直接调用了 ffio_open_whitelist. ff应该是ffmpeg的前面两个ff的意思吧.

int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,                         const AVIOInterruptCB *int_cb, AVDictionary **options,                         const char *whitelist                        ){    URLContext *h;    int err;    // 1 url open,这一步是重点    err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist);    // 2 io open,没有真正的open操作,而是赋值    err = ffio_fdopen(s, h);    return 0;}int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,                         const AVIOInterruptCB *int_cb, AVDictionary **options, const char *whitelist){    AVDictionary *tmp_opts = NULL;    AVDictionaryEntry *e;    // 又是在alloc,在alloc中有很多操作.    int ret = ffurl_alloc(puc, filename, flags, int_cb);    ret = ffurl_connect(*puc, options);}

在ffurl_alloc函数中进行了选择.很绕的,从 io_open_defualt->ffio_open_whitelist->ffurl_open_whitelist->ffurl_alloc

int ffurl_alloc(URLContext **puc, const char *filename, int flags,                const AVIOInterruptCB *int_cb){    URLProtocol *p = NULL;    p = url_find_protocol(filename);    if (p)       return url_alloc_for_protocol(puc, p, filename, flags, int_cb);}#define URL_SCHEME_CHARS                        \    "abcdefghijklmnopqrstuvwxyz"                \    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"                \    "0123456789+-."static struct URLProtocol *url_find_protocol(const char *filename){    URLProtocol *up = NULL;    char proto_str[128], proto_nested[128], *ptr;    //若strspn()返回的数值为n,则代表字符串s 开头连续有n 个字符都是属于字符串accept内的字符。    // 如果是rtmp://...,则找到了rtmp    size_t proto_len = strspn(filename, URL_SCHEME_CHARS);    if (filename[proto_len] != ':' &&        (strncmp(filename, "subfile,", 8) || !strchr(filename + proto_len + 1, ':')) ||        is_dos_path(filename))        strcpy(proto_str, "file");   //如果没有提出协议,则是file    else        av_strlcpy(proto_str, filename,                   FFMIN(proto_len + 1, sizeof(proto_str))); //这一步是rtmp    av_strlcpy(proto_nested, proto_str, sizeof(proto_nested));    //遍历proto,通过proto的name来进行判断    while (up = ffurl_protocol_next(up)) {        if (!strcmp(proto_str, up->name))            break;        if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&            !strcmp(proto_nested, up->name))            break;    }    return up;}

找到了proto后, ffurl_connect 做了啥?

int ffurl_connect(URLContext *uc, AVDictionary **options){    ...    //重点是不是在这里:    err =        uc->prot->url_open2 ? uc->prot->url_open2(uc,                                                  uc->filename,                                                  uc->flags,                                                  options) :        uc->prot->url_open(uc, uc->filename, uc->flags);    uc->is_connected = 1;    return 0;}

uc->prot 是 找到的proto,在 ffurl_alloc->url_alloc_for_protocol 进行了赋值. 调用了URLProtocol url_open.

在init_input中,确定了proto,开始探测出format.init_input->av_probe_input_format2->av_probe_input_format3.

//av_probe_input_format2 中获取到了数据int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt,                          const char *filename, void *logctx,                          unsigned int offset, unsigned int max_probe_size){    AVProbeData pd = { filename ? filename : "" };    uint8_t *buf = NULL;    int ret = 0, probe_size, buf_offset = 0;    int score = 0;    int ret2;    //PROBE_BUF_MIN = 2048    for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt;             probe_size = FFMIN(probe_size << 1,FFMAX(max_probe_size, probe_size + 1)))     {        /* Read probe data. */        if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)            goto fail;        // 1 读数据        if ((ret = avio_read(pb, buf + buf_offset,                             probe_size - buf_offset)) < 0) {        }        // 2 Guess file format.         *fmt = av_probe_input_format2(&pd, 1, &score);        if (*fmt) {        }    }}// av_probe_input_format2调用了av_probe_input_format3, 这个函数是比较慢的AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened,                                      int *score_ret){    fmt = NULL;    //遍历fmt    while ((fmt1 = av_iformat_next(fmt1))) {        score = 0;        if (fmt1->read_probe) { //如果有探测函数            score = fmt1->read_probe(&lpd); //开始探测,得分            if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) { //扩展名一样,扩展名也是一个重要参考                switch (nodat) {                case NO_ID3:                    score = FFMAX(score, 1);                    break;                    ...                }            }        } else if (fmt1->extensions) { //无探测函数,有扩展名            if (av_match_ext(lpd.filename, fmt1->extensions))                score = AVPROBE_SCORE_EXTENSION;        }        //比较 mime_type 类型        if (av_match_name(lpd.mime_type, fmt1->mime_type))            score =  FFMAX(score, AVPROBE_SCORE_MIME);        //找打分最高的fmt        if (score > score_max) {            score_max = score;            fmt       = fmt1;        } else if (score == score_max)            fmt = NULL;    }    return fmt;}

在init_input中,确定了proto和format.

总结一下有一个逻辑,列出了几个重要的函数

Created with Raphaël 2.1.0Startmainstream_openread_threadavformat_open_inputinit_inputs->io_open=io_open_defualturl_find_protocolav_probe_input_buffer2av_probe_input_format3

下一步就得知道codec的确定啦.

2 如何确定codec

先看初始化,也是用列表来把多个codec串起来.

av_register_all->avcodec_register_all

void avcodec_register_all(void){    static int initialized;    if (initialized)        return;    initialized = 1;    //编码器    REGISTER_ENCDEC (AAC,               aac);    //解码器    REGISTER_DECODER(H264,              h264);}//注册编码器#define REGISTER_ENCODER(X, x)                                          \    {                                                                   \        extern AVCodec ff_##x##_encoder;                                \        if (CONFIG_##X##_ENCODER)                                       \            avcodec_register(&ff_##x##_encoder);                        \    }//注册解码器#define REGISTER_DECODER(X, x)                                          \    {                                                                   \        extern AVCodec ff_##x##_decoder;                                \        if (CONFIG_##X##_DECODER)                                       \            avcodec_register(&ff_##x##_decoder);                        \    }//注册编码器和解码器#define REGISTER_ENCDEC(X, x) REGISTER_ENCODER(X, x); REGISTER_DECODER(X, x)

ffmpeg是不支持H264的编码的,主要看一下H264的解码对象 ff_h264_decoder

//结构体这么赋值,也蛮好的.static const AVClass h264_class = {    .class_name = "H264 Decoder",    .item_name  = av_default_item_name,    .option     = h264_options,    .version    = LIBAVUTIL_VERSION_INT,};AVCodec ff_h264_decoder = {    .name                  = "h264",    .long_name             = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"),    .type                  = AVMEDIA_TYPE_VIDEO,    .id                    = AV_CODEC_ID_H264,    .priv_data_size        = sizeof(H264Context),    .init                  = ff_h264_decode_init,    .close                 = h264_decode_end,    .decode                = h264_decode_frame,    .capabilities          = /*AV_CODEC_CAP_DRAW_HORIZ_BAND |*/ AV_CODEC_CAP_DR1 |                             AV_CODEC_CAP_DELAY | AV_CODEC_CAP_SLICE_THREADS |                             AV_CODEC_CAP_FRAME_THREADS,    .caps_internal         = FF_CODEC_CAP_INIT_THREADSAFE,    .flush                 = flush_dpb,    .init_thread_copy      = ONLY_IF_THREADS_ENABLED(decode_init_thread_copy),    .update_thread_context = ONLY_IF_THREADS_ENABLED(ff_h264_update_thread_context),    .profiles              = NULL_IF_CONFIG_SMALL(ff_h264_profiles),    .priv_class            = &h264_class,};

里面有一个name和id,任意一个都可以找到.初始化后,确定codec的逻辑要直接到fmt的里面去.以FLV为例子,ff_live_flv_demuxer ,这可能要设计到FLV的封装,这里不详解了.如何解析不是这一篇的该讲的.

//肯定在这里两个函数中//.read_header    = flv_read_header,  //read header只是解析了flv的header,没有解析到tag hader//.read_packet    = flv_read_packet,  //这个函数很复杂,挑着看.static int flv_read_packet(AVFormatContext *s, AVPacket *pkt){retry:    //先解析,重点在于type    pos  = avio_tell(s->pb);    type = (avio_r8(s->pb) & 0x1F); //有三种类型script,vedio,audio    ...    if (type == FLV_TAG_TYPE_VIDEO) {        stream_type = FLV_STREAM_TYPE_VIDEO;        flags    = avio_r8(s->pb);       //flags可以找到codec id        size--;    }     if (stream_type == FLV_STREAM_TYPE_VIDEO) {        size -= flv_set_video_codec(s, st, flags & FLV_VIDEO_CODECID_MASK, 1);    }     return ret;}static int flv_set_video_codec(AVFormatContext *s, AVStream *vstream,                               int flv_codecid, int read){    AVCodecContext *vcodec = vstream->codec;    switch (flv_codecid) {    ....    case FLV_CODECID_H264:  // FLV_CODECID_H264    = 7,        vcodec->codec_id = AV_CODEC_ID_H264;  //这就和ff_h264_decoder.id对应上了        vstream->need_parsing = AVSTREAM_PARSE_HEADERS;        return 3;       ...    }    return 0;}

在封装逻辑(fmt,demux,有多个解释.)中找到了 codec_id,哪里有用到? read_thread->avformat_find_stream_info->find_decoder

static const AVCodec *find_decoder(AVFormatContext *s, AVStream *st, enum AVCodecID codec_id){    if (st->codec->codec)        return st->codec->codec;    switch (st->codec->codec_type) {    case AVMEDIA_TYPE_VIDEO:        if (s->video_codec)    return s->video_codec;        break;    case AVMEDIA_TYPE_AUDIO:        if (s->audio_codec)    return s->audio_codec;        break;    case AVMEDIA_TYPE_SUBTITLE:        if (s->subtitle_codec) return s->subtitle_codec;        break;    }    return avcodec_find_decoder(codec_id);}AVCodec *avcodec_find_decoder(enum AVCodecID id){    return find_encdec(id, 0);}static AVCodec *find_encdec(enum AVCodecID id, int encoder){    AVCodec *p, *experimental = NULL;    p = first_avcodec;    id= remap_deprecated_codec_id(id);    //遍历 所有codec    while (p) {        if ((encoder ? av_codec_is_encoder(p) : av_codec_is_decoder(p)) &&            p->id == id) {  //找id的            if (p->capabilities & AV_CODEC_CAP_EXPERIMENTAL && !experimental) {                experimental = p;            } else                return p;        }        p = p->next;    }    return experimental;}

每个封装都有不同的解析包的逻辑,只有把包解析了才知道codec.

这一篇可以终结了.还多没明白的地方,譬如Proto如何file和rtmp读写. 先不用急. 把这个点搞明白之后,再下一个点.

下一篇来分析ffplay test.flv的整体逻辑. 之后再分析file和rtmp的读写.

0 0
原创粉丝点击