FFmpeg libavformat协议框架代码分析

1,872 阅读3分钟

背景

ffmpeg有自己的一套网络IO代码,但是相对比较简单,现在需要使用自研的网络库接管ffmpeg的网络IO功能。方法有两种:

  1. 在原本的url前面加127.0.0.1的方式,走本地代理的方式
  2. 新增自己的协议,替换原本的http协议 第一种方法比较简单,本文主要介绍第二种方法。
    首先介绍下libavformat协议框架,然后在介绍下如何在ffmpeg中新增一个自定义的协议。

libavformat协议框架

相关文件

libavformat/protocol_list.c
libavformat/protocols.c
libavformat/avio.c

protocol_list.c 是在执行 ./configure 根据参数而动态生成的代码。默认参数生成的代码如下:

static const URLProtocol *url_protocols[] = {
    ...
    &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 };

url_protocols是一个静态数组,存储了http,tcp,udp,rtmp等常用协议的实现。
做一个与本文无关的友情链接 ffmpeg支持的所有协议说明

关键的URLProtocol现在出场

typedef struct URLProtocol {
    const char *name;
    int     (*url_open2)(URLContext *h, const char *url, int flags, AVDictionary **options);
    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_shutdown)(URLContext *h, int flags);
    int     (*url_close)(URLContext *h);    
    int priv_data_size;
    const AVClass *priv_data_class;
    int flags;
    ......
} URLProtocol;

URLProtocol可以理解成一个通用的接口,avformat中的每种协议都实现URLProtocol中自己感兴趣的函数。 此处URLProtocol就是用C语言用函数指针实现了C++多态的一种技术。

先来简单了解下HTTP协议。HTTP协议是应用层协议,需要依赖下一层的传输层协议来做二进制数据的传输。
所以,当用户使用http或https请求资源时,ffmpeg如何找到具体的对应协议的处理代码呢?
在libavformat/avio.c的

static const struct URLProtocol *url_find_protocol(const char *filename)
{
	...
    for (i = 0; protocols[i]; i++) {
         const URLProtocol *up = protocols[i];
        if (!strcmp(proto_str, up->name)) { // 通过URLProtocol中的name字段来做匹配
            av_freep(&protocols);
            return up;
        }
    }
    ...
    return NULL;
}

在此,看下libavformat/http.c中协议注册的代码,则可以很清楚的看到http协议的name字段是http

const URLProtocol ff_http_protocol = {
    .name                = "http",
    .url_open2           = http_open,
    .url_accept          = http_accept,
    .url_handshake       = http_handshake,
    .url_read            = http_read,
    .url_write           = http_write,
    .url_seek            = http_seek,
    .url_close           = http_close,
    .url_get_file_handle = http_get_file_handle,
    .url_get_short_seek  = http_get_short_seek,
    .url_shutdown        = http_shutdown,
    .priv_data_size      = sizeof(HTTPContext),
    .priv_data_class     = &http_context_class,
    .flags               = URL_PROTOCOL_FLAG_NETWORK,
    .default_whitelist   = "http,https,tls,rtp,tcp,udp,crypto,httpproxy"
};

同理libavformat/tcp.c中的tcp协议也是一样的

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,
};

则形如下面的url在请求时,url_find_protocol通过匹配字符http则找到了http协议的处理函数

http://1.2.3.4/stream-123.flv

在libavformat/http.c中的http_open_cnx_internal函数,则根据请求url中的http或https来决定下一层传输层的协议

static int http_open_cnx_internal(URLContext *h, AVDictionary **options) {
    const char *lower_proto = "tcp"; // 默认使用tcp协议传输
    
    if (!strcmp(proto, "https")) {
        lower_proto = "tls"; // 应用层是https协议,传输层使用tls协议
        use_proxy   = 0;
        if (port < 0)
            port = 443;  // tls默认443端口
    }
    if (port < 0)
        port = 80;  // tcp默认80端口

    ff_url_join(buf, sizeof(buf), lower_proto, NULL, hostname, port, NULL);
}

通过ff_url_join的拼接形成了

tcp://116.162.86.92:80

然后再次调用avio.c中的ffurl_open_whitelist函数,进而又通过url_find_protocol函数查找到tcp协议的处理函数。

在ffmpeg中新增自定义的live协议

新增自定义的live协议的步骤是:

  1. 新建live.c文件,实现URLProtocol中感兴趣的函数,代码如下
class FFLiveProtocol
{
public:
    static int live_open(URLContext *h, const char *url, int flags, AVDictionary **options) {
    	return 0;
    }    
    static int live_read(URLContext *h, unsigned char *buf, int size) {
    	return 0;
    }

    static int live_write(URLContext *h, const unsigned char *buf, int size) {
    	return 0;
    }
    static int live_close(URLContext *h) {
        return 0;
    }
};

URLProtocol g_ff_live_protocol = {
    .name                = "live",
    .url_open2           = FFLiveProtocol::live_open,
    .url_read            = FFLiveProtocol::live_read,
    .url_write           = FFLiveProtocol::live_write,
    .url_close           = FFLiveProtocol::live_close,
    .priv_data_size      = sizeof(FF_LiveContext),
    .flags               = URL_PROTOCOL_FLAG_NETWORK,
    .priv_data_class     = &live_class,
};
  1. 在./configure参数选项中开启live协议 先查看帮助:
# ./configure --help |egrep "enable-protocol"
  --enable-protocol=NAME   enable protocol NAME

则新增参数如:

--enable-protocol=live

完成这两步后,则可以将原有url修改为

live://1.2.3.4/stream-123.flv

到此,就完成了协议的替换了。后面live协议中的实现就任由你折腾了。