ffmpeg面向对象——拉流协议匹配机制探索

目录

  • 1.URLProtocol类
  • 2.协议匹配的核心接口
    1. URLContext类
    1. 综合调用流程图
  • 5.rtsp拉流协议匹配流程图及对象图
    • 5.1 rtsp拉流协议调用流程图
    • 5.2 rtsp拉流协议对象图
  • 6.本地文件调用流程图及对象图
    • 6.1 本地文件调用流程图
    • 6.2 本地文件对象图
  • 7.内存数据调用流程图及对象图
    • 7.1 内存数据调用流程图
    • 7.2 内存数据对象图
  • 8 filename取值规则
  • 9.小结

如果让你写个拉流程序,输入拉流地址,可以是本地文件路径,可以是内存数据,可以网络流媒体传输协议比如http或者rtsp等,那么不同拉流地址 ,调用底层的读写函数不一样,如何统一操作呢?探索下ffmpeg是怎么统一这种问题的。

ffmpeg抽象出了url协议类------URLProtocol类------来统一这种操作。

(雷神有画出使用时的各个协议雷神相关博客,但是没有看到其匹配机制,且只有函数调用图,没有对象图。我这个探索,可以算是补充)

1.URLProtocol类

c 复制代码
typedef struct URLProtocol {
    const char *name;
    int     (*url_open)( URLContext *h, const char *url, int flags);
    /**
     * This callback is to be used by protocols which open further nested
     * protocols. options are then to be passed to ffurl_open_whitelist()
     * or ffurl_connect() for those nested protocols.
     */
    int     (*url_open2)(URLContext *h, const char *url, int flags, AVDictionary **options);
    int     (*url_accept)(URLContext *s, URLContext **c);
    int     (*url_handshake)(URLContext *c);

    /**
     * Read data from the protocol.
     * If data is immediately available (even less than size), EOF is
     * reached or an error occurs (including EINTR), return immediately.
     * Otherwise:
     * In non-blocking mode, return AVERROR(EAGAIN) immediately.
     * In blocking mode, wait for data/EOF/error with a short timeout (0.1s),
     * and return AVERROR(EAGAIN) on timeout.
     * Checking interrupt_callback, looping on EINTR and EAGAIN and until
     * enough data has been read is left to the calling function; see
     * retry_transfer_wrapper in avio.c.
     */
    int     (*url_read)( URLContext *h, unsigned char *buf, int size);
    int     (*url_write)(URLContext *h, const unsigned char *buf, int size);
    int64_t (*url_seek)( URLContext *h, int64_t pos, int whence);
    int     (*url_close)(URLContext *h);
    int (*url_read_pause)(URLContext *h, int pause);
    int64_t (*url_read_seek)(URLContext *h, int stream_index,
                             int64_t timestamp, int flags);
    int (*url_get_file_handle)(URLContext *h);
    int (*url_get_multi_file_handle)(URLContext *h, int **handles,
                                     int *numhandles);
    int (*url_get_short_seek)(URLContext *h);
    int (*url_shutdown)(URLContext *h, int flags);
    const AVClass *priv_data_class;
    int priv_data_size;
    int flags;
    int (*url_check)(URLContext *h, int mask);
    int (*url_open_dir)(URLContext *h);
    int (*url_read_dir)(URLContext *h, AVIODirEntry **next);
    int (*url_close_dir)(URLContext *h);
    int (*url_delete)(URLContext *h);
    int (*url_move)(URLContext *h_src, URLContext *h_dst);
    const char *default_whitelist;
} URLProtocol;

这其实就是高级语言(如c++等)的接口类------需要各个子类实现的接口。

可以看到,这类,抽象出来的共性的接口------读写文件等。如果是本地文件,那就调用fopen等系统调用,如果是内存数据则调用用户自定义的读取函数,如果是网络流媒体协议,则是调用socket网络编程接口------这样实现了多态。

因此,ffmpeg实例化了很多它支持的URLProtocol类对象,放到了全局表格url_protocols中,比如

c 复制代码
//在libavformat/protocol_list.c
static const URLProtocol *url_protocols[] = {
    &ff_file_protocol,
    &ff_hls_protocol,
    &ff_http_protocol,
    &ff_httpproxy_protocol,
    &ff_rtmp_protocol,
    &ff_rtmpt_protocol,
    &ff_rtp_protocol,
    &ff_srtp_protocol,
    &ff_tcp_protocol,
    &ff_udp_protocol,
    &ff_unix_protocol,
    NULL };

注意:protocol_list.c是configure生成的,也就是说下载的ffmpeg源码中是不存在的,这个是ffmpeg可裁剪的一个举措。------可以配置configure来选择支持哪些协议,和linux的menuconfig类似(不得不觉得互相学习)。

和这篇《ffmpeg面向对象-rtsp拉流相关对象》探索过的ffmpeg统一输入格式的方式一样。这个可以说是ffmpeg的特点的,一个开源项目有其遵循的法则,指定的规则,通一知百。

2.协议匹配的核心接口

ffmpeg实现协议匹配的核心接口是url_find_protocol。

c 复制代码
#define URL_SCHEME_CHARS                        \
    "abcdefghijklmnopqrstuvwxyz"                \
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"                \
    "0123456789+-."

static const struct URLProtocol *url_find_protocol(const char *filename)
{
    const URLProtocol **protocols;
    char proto_str[128], proto_nested[128], *ptr;
    size_t proto_len = strspn(filename, URL_SCHEME_CHARS);
    int i;

    if (filename[proto_len] != ':' &&
        (strncmp(filename, "subfile,", 8) || !strchr(filename + proto_len + 1, ':')) ||
        is_dos_path(filename))
        strcpy(proto_str, "file");
    else
        av_strlcpy(proto_str, filename,
                   FFMIN(proto_len + 1, sizeof(proto_str)));

    av_strlcpy(proto_nested, proto_str, sizeof(proto_nested));
    if ((ptr = strchr(proto_nested, '+')))
        *ptr = '\0';

    protocols = ffurl_get_protocols(NULL, NULL);
    if (!protocols)
        return NULL;
    
    for (i = 0; protocols[i]; i++) 
    {
            const URLProtocol *up = protocols[i];
        if (!strcmp(proto_str, up->name)) 
        {
            av_freep(&protocols);
            return up;
        }
        if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&
            !strcmp(proto_nested, up->name)) 
        {
            av_freep(&protocols);
            return up;
        }
    }
    
    av_freep(&protocols);
    if (av_strstart(filename, "https:", NULL) || av_strstart(filename, "tls:", NULL))
        av_log(NULL, AV_LOG_WARNING, "https protocol not found, recompile FFmpeg with "
                                     "openssl, gnutls or securetransport enabled.\n");

    return NULL;
}

可以看到,它先判断传入的filename是文件路径还是流传输url,前者把proto_str设为"file",后者取url的传输协议头比如"tcp"设给proto_str。

ffurl_get_protocols主要是获取url_protocols表格里的各个协议类对象地址(内部做了次拷贝,反正不用管)。

再for遍历比较proto_str和各个协议类对象的name是否相同,这样就从url_protocols协议表格中匹配到对应协议类对象了。

其实到这里,匹配机制完毕,接下来就是追踪调用源头。

这个匹配函数最后返回对应协议类对象的地址------这个地址给谁了呢?这得看谁调它了,调它的有两处,一个是ffurl_alloc(),一个是avio_find_protocol_name。前者取匹配到的协议对象地址,后者是取协议对象的名字,显而易见,前者是比较重要的。

c 复制代码
int ffurl_alloc(URLContext **puc, const char *filename, int flags,
                const AVIOInterruptCB *int_cb)
{
    const URLProtocol *p = NULL;

    p = url_find_protocol(filename);
    if (p)
       return url_alloc_for_protocol(puc, p, filename, flags, int_cb);

    *puc = NULL;
    return AVERROR_PROTOCOL_NOT_FOUND;
}

可以看到,协议类对象地址作为形参传给url_alloc_for_protocol函数了,看其最终到哪里了?

c 复制代码
static int url_alloc_for_protocol(URLContext **puc, const URLProtocol *up,
                                  const char *filename, int flags,
                                  const AVIOInterruptCB *int_cb)
{
    URLContext *uc;
    int err;
	......
    uc = av_mallocz(sizeof(URLContext) + strlen(filename) + 1);
    if (!uc) {
        err = AVERROR(ENOMEM);
        goto fail;
    }
    uc->av_class = &ffurl_context_class;
    uc->filename = (char *)&uc[1];
    strcpy(uc->filename, filename);
    uc->prot            = up;
    uc->flags           = flags;
    uc->is_streamed     = 0; /* default = not streamed */
    uc->max_packet_size = 0; /* default: stream file */
    ......
}

剔除无关代码后,可以看到最终是放到URLContext类对象的prot成员中------看来URLContext类是URLProtocol 类的管理者。

3. URLContext类

此时对象图如下:

URLContext协议上下文类来管理匹配到的协议类。其URLContext类定义如下

c 复制代码
typedef struct URLContext {
    const AVClass *av_class;    /**< information for av_log(). Set by url_open(). */
    const struct URLProtocol *prot;
    void *priv_data;
    char *filename;             /**< specified URL */
    int flags;
    int max_packet_size;        /**< if non zero, the stream is packetized with this max packet size */
    int is_streamed;            /**< true if streamed (no seek possible), default = false */
    int is_connected;
    AVIOInterruptCB interrupt_callback;
    int64_t rw_timeout;         /**< maximum time to wait for (network) read/write operation completion, in mcs */
    const char *protocol_whitelist;
    const char *protocol_blacklist;
    int min_packet_size;        /**< if non zero, the stream is packetized with this min packet size */
} URLContext;

可以发现,它第一个成员是AVClass 的指针成员,此时(url_alloc_for_protocol函数处理后),指向了ffurl_context_class,《ffmpeg面向对象------参数配置机制及其设计模式探索》探索过,凡是戴上这个帽子的,它就成为了可配参数业务类,不再赘述。可见它的可配的参数就3个,如下

白名单,黑名单,读写超时时间。最终是设置到这个协议管理类里了。

4. 综合调用流程图

那么又是谁调用ffurl_alloc的呢?

看到被调用的地方有很多,但是 ffurl_open_whitelist 是个关键调用------它是内存数据、本地文件或者流媒体协议最终都会调用的

具体如何调用的,见如下综合调用流程。

到此,协议匹配探索到源头,算是结束。

但是这个综合图把所有情况都考虑到了,但是这样很不好,很不清晰,鼻子眉毛一把抓,非常不清晰,不易于理解。应该分解下,根据业务功能单独画。

对于庞大代码,业务功能融合多的代码,要用业务线索来看------从单一的业务功能点为聚焦点,这样别的业务代码不会迷了眼,就像《ffmpeg面向对象------参数配置机制及其设计模式探索》一样,只聚焦于参数流向,无关代码,让开。

因此,按业务功能点可以拆分成网络流媒体协议匹配、本地文件路径匹配和内存数据匹配等3大块。网络流媒体协议常用的rtsp等是个代表。

5.rtsp拉流协议匹配流程图及对象图

5.1 rtsp拉流协议调用流程图

rtsp协议下,它匹配到的协议调用流程如下:

剔除了其他业务流程,瞬间干净清爽许多。

可以看到rtsp的协议匹配触发在输入格式的方法里。

5.2 rtsp拉流协议对象图

其对象图如下:

图上最下面的就是这个协议管理类URLContext 了(对象图上已标注出其匹配到的全局变量了)。

在rtsp拉流协议下,URLContext作为的输入格式AVInputFormat中私有数据的成员,比如rtsp的私有数据RTSPState。

c 复制代码
typedef struct RTSPState {
    const AVClass *class;             /**< Class for private options. */
    URLContext *rtsp_hd; /* RTSP TCP connection handle */

    /** number of items in the 'rtsp_streams' variable */
    int nb_rtsp_streams;

    struct RTSPStream **rtsp_streams; /**< streams in this session */

    /** indicator of whether we are currently receiving data from the
     * server. Basically this isn't more than a simple cache of the
     * last PLAY/PAUSE command sent to the server, to make sure we don't
     * send 2x the same unexpectedly or commands in the wrong state. */
    enum RTSPClientState state;
	......
	}

6.本地文件调用流程图及对象图

6.1 本地文件调用流程图

待探索。

6.2 本地文件对象图

待探索。

7.内存数据调用流程图及对象图

7.1 内存数据调用流程图

待探索。

7.2 内存数据对象图

待探索。

8 filename取值规则

ffmpeg封装的基础接口是url_find_protocol的形参filename取值范围,前面没说,为了过流程,就忽略了。

拿url_protocols表格中ffmpeg支持的几个url协议上,探索下这个filename的取值范围是啥。

c 复制代码
const URLProtocol ff_file_protocol = {
    .name                = "file",
    .url_open            = file_open,
    .url_read            = file_read,
    .url_write           = file_write,
    .url_seek            = file_seek,
    .url_close           = file_close,
    .url_get_file_handle = file_get_handle,
    .url_check           = file_check,
    .url_delete          = file_delete,
    .url_move            = file_move,
    .priv_data_size      = sizeof(FileContext),
    .priv_data_class     = &file_class,
    .url_open_dir        = file_open_dir,
    .url_read_dir        = file_read_dir,
    .url_close_dir       = file_close_dir,
    .default_whitelist   = "file,crypto,data"
};
c 复制代码
const URLProtocol ff_hls_protocol = {
    .name           = "hls",
    .url_open       = hls_open,
    .url_read       = hls_read,
    .url_close      = hls_close,
    .flags          = URL_PROTOCOL_FLAG_NESTED_SCHEME,
    .priv_data_size = sizeof(HLSContext),
};
c 复制代码
const URLProtocol ff_tcp_protocol = {
    .name                = "tcp",
    .url_open            = tcp_open,
    .url_accept          = tcp_accept,
    .url_read            = tcp_read,
    .url_write           = tcp_write,
    .url_close           = tcp_close,
    .url_get_file_handle = tcp_get_file_handle,
    .url_get_short_seek  = tcp_get_window_size,
    .url_shutdown        = tcp_shutdown,
    .priv_data_size      = sizeof(TCPContext),
    .flags               = URL_PROTOCOL_FLAG_NETWORK,
    .priv_data_class     = &tcp_class,
};
c 复制代码
const URLProtocol ff_udp_protocol = {
    .name                = "udp",
    .url_open            = udp_open,
    .url_read            = udp_read,
    .url_write           = udp_write,
    .url_close           = udp_close,
    .url_get_file_handle = udp_get_file_handle,
    .priv_data_size      = sizeof(UDPContext),
    .priv_data_class     = &udp_class,
    .flags               = URL_PROTOCOL_FLAG_NETWORK,
};

从这些支持的协议类型看,filename的取值范围其实就是在这些协议类对象里,那么代码里怎么知道是哪个呢?不同协议类型的name是不一样的,比如上面的有"udp"、"tcp"、"file"等等,就是filename传递进来的时候肯定是有处理过的,那么在哪里处理比价合理呢?在各个业务功能模块里比较合理。比如rtsp拉流协议的输入格式AVInputFormat(网上称之解复用器,其实我觉得叫复合数据分离器 比较贴切,demux我一直翻译成分离器 比较容易理解,一分多,mux融合数据,多合一)里rtsp_read_header这个回调里调用的ff_rtsp_connect中,进行了处理:

可以看到,会把rtsp拉流url比如"rtsp://ip:port/xxx"给改成"tcp://ip:port/xxx",这样在url_find_protocol中就能截取到"tcp"从而rtsp的拉流协议会匹配到ff_tcp_protocol。

其他类似,肯定有处理的地方。

9.小结

和ffmpeg的输入格式统一到一个类和一个表格一样,这个拉流协议匹配也是统一到一个类和一个表格,且都是configure可裁剪的。及其相似,同类规则,我相信其他模块也是类似。

其实应该倒看,章节倒看会比较不错,比如5-4-3-2-1或者6-4-3-2-1或者7-4-3-2-1。

相关推荐
Moonnnn.41 分钟前
51单片机——程序执行过程(手工汇编)
汇编·笔记·嵌入式硬件·学习·51单片机
大宝剑1701 小时前
blender学习25.3.8
学习·blender
.m2 小时前
使用格式工厂提取视频中的音频
ffmpeg
flashier7 小时前
C语言 进阶指针学习笔记
c语言·笔记·学习
大白的编程日记.7 小时前
【Linux学习笔记】Linux基本指令分析和权限的概念
linux·笔记·学习
螺旋式上升abc7 小时前
GO语言学习笔记
笔记·学习·golang
W起名有点难7 小时前
前端学习——CSS
前端·css·学习
Moonnnn.8 小时前
51单片机——汇编工程建立、仿真、调试全过程
汇编·笔记·嵌入式硬件·学习·51单片机
EnigmaCoder9 小时前
蓝桥杯刷题周计划(第二周)
学习·算法·蓝桥杯
银河小铁骑plus10 小时前
Go学习笔记:基础语法6
笔记·学习·golang