ffmpeg系列-協議操作解析-AVIOContext,URLContext,URLProtocol,HTTPContext

1.協議操作對象結構

協議操作對象結構:

image

協議操作的頂層結構是AVIOContext,這個對象實現了帶緩衝的讀寫操作;FFMPEG的輸入對象AVFormatContext的pb字段指向一個AVIOContext。

AVIOContext的opaque實際指向一個URLContext對象,這個對象封裝了協議對象及協議操作對象,其中prot指向具體的協議操作對象(如URLProtocol),priv_data指向具體的協議對象(如HTTPContext或者FileContext)。

URLProtocol爲協議操作對象,針對每種協議,會有一個這樣的對象,每個協議操作對象和一個協議對象關聯,比如,文件操作對象爲ff_file_protocol,它關聯的結構體是FileContext。http協議操作對象爲ff_http_protocol,它的關聯的結構體是HttpContext。

2.代碼分析

2.1初始化AVIOContext函數調用關係

初始化AVIOFormat函數調用關係:

image

我們先從下面到上面來分析源碼。先分析協議中具體實現以及URLProtocol以及URLContext。

URLProtocol是FFMPEG操作文件的結構(包括文件,網絡數據流等等),包括open、close、read、write、seek等操作。

在av_register_all()函數中,通過調用REGISTER_PROTOCOL()宏,所有的URLProtocol都保存在以first_protocol爲鏈表頭的鏈表中

URLProtocol結構體的定義爲:

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()/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);
    int priv_data_size;
    const AVClass *priv_data_class;
    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;

以HTTP協議爲例,ff_http_protocol

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

從中可以看出,.priv_data_size的值爲sizeof(HTTPContext),即ff_http_protocol和HttpContext相關聯。

HttpContext對象的定義爲:

typedef struct HTTPContext {
    const AVClass *class;
    URLContext *hd;
    unsigned char buffer[BUFFER_SIZE], *buf_ptr, *buf_end;
    int line_count;
    int http_code;
    /** 
    如果使用Transfer-Encoding:chunked,也就是代表這個報文采用了分塊編碼,不然設置爲-1
	Used if "Transfer-Encoding: chunked" otherwise -1. 
    分塊編碼:
    報文中的實體需要改爲用一系列的分塊來傳輸,每個分塊包含十六進制的長度值和數據,
    長度值獨佔一行,長度不包括它結尾的 CRLF(\r\n),也不包括分塊數據結尾的 CRLF。
    最後一個分塊長度值必須爲 0,對應的分塊數據沒有內容,表示實體結束
    如:服務端
    	sock.write('HTTP/1.1 200 OK\r\n');
        sock.write('Transfer-Encoding: chunked\r\n');
        sock.write('\r\n');
        sock.write('b\r\n');//指定長度值11
        sock.write('01234567890\r\n');//數據
        sock.write('5\r\n');//執行下面長度值5
        sock.write('12345\r\n');//數據
        sock.write('0\r\n');//最後一個分塊是0
        sock.write('\r\n');
    **/
    uint64_t chunksize;
	//off  buf偏移   end_off->請求頭:range的結尾值
    uint64_t off, end_off, filesize;
    char *location;
    HTTPAuthState auth_state;
    HTTPAuthState proxy_auth_state;
    char *http_proxy;
    char *headers;
    char *mime_type;
    char *user_agent;
#if FF_API_HTTP_USER_AGENT
    char *user_agent_deprecated;
#endif
    char *content_type;
    /* 
	如果服務器設置正確處理連接:關閉並將關閉,也就是處理了請求頭中的Connetion
	如果Connection是close的話,處理完後斷開連接。willclose設置1,在解析請求頭中
	也就是這個變量代表Connection是否是close  
    */
    int willclose;
    int seekable;           /**< Control seekability, 0 = disable, 1 = enable, -1 = probe. */
    int chunked_post;
    /* A flag which indicates if the end of chunked encoding has been sent. */
    int end_chunked_post;
    /* A flag which indicates we have finished to read POST reply. */
    int end_header;
    /* A flag which indicates if we use persistent connections. */
    int multiple_requests;
    uint8_t *post_data;
    int post_datalen;
    int is_akamai;
    int is_mediagateway;
    char *cookies;          ///< holds newline (\n) delimited Set-Cookie header field values (without the "Set-Cookie: " field name)
    /* A dictionary containing cookies keyed by cookie name */
    AVDictionary *cookie_dict;
    int icy;
    /* how much data was read since the last ICY metadata packet */
    uint64_t icy_data_read;
    /* after how many bytes of read data a new metadata packet will be found */
    uint64_t icy_metaint;
    char *icy_metadata_headers;
    char *icy_metadata_packet;
    AVDictionary *metadata;
#if CONFIG_ZLIB
    int compressed;//如果服務器返回客戶端的內容正文壓縮的話
    z_stream inflate_stream;
    uint8_t *inflate_buffer;
#endif /* CONFIG_ZLIB */
    AVDictionary *chained_options;
    int send_expect_100;
    char *method;
    int reconnect;
    int reconnect_at_eof;
    int reconnect_streamed;
    int reconnect_delay;
    int reconnect_delay_max;
    int listen;
    char *resource;
    int reply_code;
    int is_multi_client;
    HandshakeState handshake_step;
    int is_connected_server;
} HTTPContext;

返回去看ff_http_protocol裏的函數指針,以url_read舉例,它指向http_read函數,看一下這個函數。

/**
*流數據讀取
**/
static int http_read(URLContext *h, uint8_t *buf, int size)
{
    HTTPContext *s = h->priv_data;//URLContext的priv_data指向HttpContext
    if (s->icy_metaint > 0) {
        size = store_icy(h, size);
        if (size < 0)
            return size;
    }

    size = http_read_stream(h, buf, size);
    if (size > 0)
        s->icy_data_read += size;
    return size;
}

從上面的分析中,我們知道了協議的具體實現以及URLProtocol以及URLContext結構之間的關係。下面我們開始從上面到下面的流程來分析:

avformat_open_input()函數調用init_input/utils.c/libavformat
,最後調用到avio_open()然後調用到avio_open2()/aviobuf.c/libavformat函數,然後調用到ffio_open_whitelist/aviobuf.c,下面看一下這個函數。

/**
* 按照協議名單打開協議,並初始化一個AVIOContext並賦值
**/
int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char *blacklist
                        )
{
    URLContext *h;
    int err;
    //調用avio.c中的按照白名單打開協議
    //申請協議空間與協議查找以及建立連接(調用協議中open函數,如http_open)
    //調用ffurl_open()申請創建了一個URLContext對象並打開了文件(也就是建立連接等)
    err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);
    if (err < 0)
        return err;
	//調用ffio_fdopen()申請一個AVIOContext對象並賦初值(部分初值來自ffurl_open_whitelist中創建的URLContext)
    err = ffio_fdopen(s, h);
    if (err < 0) {//創建失敗了,關閉URLContext
        ffurl_close(h);
        return err;
    }
    return 0;
}

這個函數調用ffurl_open_whitelist來創建一個URLContext,申請協議內存以及查找協議,同時建立連接,調用ffio_fdopen函數來申請一個AVIOContext對象並賦初值。

我們看下URLContext的結構體:

typedef struct URLContext {
    const AVClass *av_class;    /**< information for av_log(). Set by url_open(). */
    const struct URLProtocol *prot;//URLProtocol結構體
    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;         /**< 最大等待時間(網絡)讀/寫操作完成,在mcs,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;

從這裏我們先從ffurl_open_whitelist/avio.c函數開始看

/**
*按照白名單打開協議
*申請協議空間與協議查找以及建立連接(調用協議中open函數,如http_open)
**/
int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char* blacklist,
                         URLContext *parent)
{
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;
	//協議內存申請以及查找filename中對應的協議
    int ret = ffurl_alloc(puc, filename, flags, int_cb);
    if (ret < 0)
        return ret;
    if (parent)//傳遞過來的是NULL
        av_opt_copy(*puc, parent);
    if (options &&
        (ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;
    if (options && (*puc)->prot->priv_data_class &&
        (ret = av_opt_set_dict((*puc)->priv_data, options)) < 0)
        goto fail;

    if (!options)
        options = &tmp_opts;

    av_assert0(!whitelist ||
               !(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
               !strcmp(whitelist, e->value));
    av_assert0(!blacklist ||
               !(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
               !strcmp(blacklist, e->value));

    if ((ret = av_dict_set(options, "protocol_whitelist", whitelist, 0)) < 0)
        goto fail;

    if ((ret = av_dict_set(options, "protocol_blacklist", blacklist, 0)) < 0)
        goto fail;

    if ((ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;
//建立連接,打開協議(如,調http_open函數)
    ret = ffurl_connect(*puc, options);

    if (!ret)
        return 0;
fail:
    ffurl_close(*puc);
    *puc = NULL;
    return ret;
}

跟蹤ffurl_alloc/avio.c函數,這裏主要是協議內存申請以及查找filename中對應的協議。

/**
*協議空間申請與協議查找
**/
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;//如果沒有找到,協議不可用
    if (av_strstart(filename, "https:", NULL))//如果文件名開頭爲https,ffmpeg目前不支持https協議,需要自己移植SSL
        av_log(NULL, AV_LOG_WARNING, "https protocol not found, recompile FFmpeg with "
                                     "openssl, gnutls "
                                     "or securetransport enabled.\n");
    return AVERROR_PROTOCOL_NOT_FOUND;
}

下面看下ffurl_connect/avio.c函數,這裏主要是建立連接,打開協議(如,調http_open函數)

/**
*建立連接,打開協議
**/
int ffurl_connect(URLContext *uc, AVDictionary **options)
{
    int err;
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;

    if (!options)
        options = &tmp_opts;
	//黑名單,白名單檢測
    // Check that URLContext was initialized correctly and lists are matching if set
    av_assert0(!(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
               (uc->protocol_whitelist && !strcmp(uc->protocol_whitelist, e->value)));
    av_assert0(!(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
               (uc->protocol_blacklist && !strcmp(uc->protocol_blacklist, e->value)));

    if (uc->protocol_whitelist && av_match_list(uc->prot->name, uc->protocol_whitelist, ',') <= 0) {
        av_log(uc, AV_LOG_ERROR, "Protocol '%s' not on whitelist '%s'!\n", uc->prot->name, uc->protocol_whitelist);
        return AVERROR(EINVAL);
    }

    if (uc->protocol_blacklist && av_match_list(uc->prot->name, uc->protocol_blacklist, ',') > 0) {
        av_log(uc, AV_LOG_ERROR, "Protocol '%s' on blacklist '%s'!\n", uc->prot->name, uc->protocol_blacklist);
        return AVERROR(EINVAL);
    }

    if (!uc->protocol_whitelist && uc->prot->default_whitelist) {
        av_log(uc, AV_LOG_DEBUG, "Setting default whitelist '%s'\n", uc->prot->default_whitelist);
        uc->protocol_whitelist = av_strdup(uc->prot->default_whitelist);
        if (!uc->protocol_whitelist) {
            return AVERROR(ENOMEM);
        }
    } else if (!uc->protocol_whitelist)
        av_log(uc, AV_LOG_DEBUG, "No default whitelist set\n"); // This should be an error once all declare a default whitelist

    if ((err = av_dict_set(options, "protocol_whitelist", uc->protocol_whitelist, 0)) < 0)
        return err;
    if ((err = av_dict_set(options, "protocol_blacklist", uc->protocol_blacklist, 0)) < 0)
        return err;
	//調用port->url_open(也就是協議中的open函數,如http_open,打開了協議
    err =
        uc->prot->url_open2 ? uc->prot->url_open2(uc,
                                                  uc->filename,
                                                  uc->flags,
                                                  options) :
        uc->prot->url_open(uc, uc->filename, uc->flags);

    av_dict_set(options, "protocol_whitelist", NULL, 0);
    av_dict_set(options, "protocol_blacklist", NULL, 0);

    if (err)//如果協議打開失敗了,如http_open打開失敗了,直接返回err
        return err;
    uc->is_connected = 1;//設置is_conneced爲true,設置協議已經建立連接
    /* We must be careful here as ffurl_seek() could be slow,
     * for example for http */
    if ((uc->flags & AVIO_FLAG_WRITE) || !strcmp(uc->prot->name, "file"))
        if (!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0)
            uc->is_streamed = 1;
    return 0;
}

我們回到ffio_open_whitelist繼續從ffio_fdopen函數開始看,這裏主要是申請一個AVIOContext對象並賦初值(部分初值來自ffurl_open_whitelist中創建的URLContext)。

/**
*申請一個AVIOContext對象並賦值
*/
int ffio_fdopen(AVIOContext **s, URLContext *h)
{
    AVIOInternal *internal = NULL;
    uint8_t *buffer = NULL;
    int buffer_size, max_packet_size;

    max_packet_size = h->max_packet_size;
    if (max_packet_size) {
        buffer_size = max_packet_size; /* no need to bufferize more than one packet */
    } else {
        buffer_size = IO_BUFFER_SIZE;
    }
	//申請了一個讀寫緩衝buffer(結合文件協議,buffer大小爲IO_BUFFER_SIZE)
    buffer = av_malloc(buffer_size);//buffer_size的值取自哪裏?
    if (!buffer)
        return AVERROR(ENOMEM);

    internal = av_mallocz(sizeof(*internal));
    if (!internal)
        goto fail;

    internal->h = h;
    //對AVIOContext賦值,同時把申請到的buffer以及ffurl_read,ffurl_write,ffurl_seek的地址作爲入參被傳入
    *s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE,
                            internal, io_read_packet, io_write_packet, io_seek);
    if (!*s)//創建AVIOContext失敗的話,直接報錯
        goto fail;

    (*s)->protocol_whitelist = av_strdup(h->protocol_whitelist);
    if (!(*s)->protocol_whitelist && h->protocol_whitelist) {
        avio_closep(s);
        goto fail;
    }
    (*s)->protocol_blacklist = av_strdup(h->protocol_blacklist);
    if (!(*s)->protocol_blacklist && h->protocol_blacklist) {
        avio_closep(s);
        goto fail;
    }
    (*s)->direct = h->flags & AVIO_FLAG_DIRECT;

    (*s)->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL;
    (*s)->max_packet_size = max_packet_size;
    (*s)->min_packet_size = h->min_packet_size;//協議中獲取的urlcontext賦值給AVIOContext
    if(h->prot) {//如果URLProtStruct不爲空
        (*s)->read_pause = io_read_pause;//把ffurl_pause賦值給AVIOContext的read_pause函數
        (*s)->read_seek  = io_read_seek;//把ffurl_seek賦值給AVIOContext的read_seek函數

        if (h->prot->url_read_seek)
            (*s)->seekable |= AVIO_SEEKABLE_TIME;
    }
    (*s)->short_seek_get = io_short_seek;//根據協議調用prot(protocol)中的short_seek函數
    (*s)->av_class = &ff_avio_class;
    return 0;
fail:
    av_freep(&internal);
    av_freep(&buffer);
    return AVERROR(ENOMEM);
}

從上面可以看到調用av_malloc(buffer_size)申請了一個讀寫緩衝buffer(結合文件協議,buffer大小爲IO_BUFFER_SIZE)。

調用avio_alloc_context,來對AVIOContext賦值,同時把申請到的buffer以及ffurl_read,ffurl_write,ffurl_seek的地址作爲入參被傳入。

AVIOContext *avio_alloc_context(
                  unsigned char *buffer,
                  int buffer_size,
                  int write_flag,
                  void *opaque,
                  int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
                  int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
                  int64_t (*seek)(void *opaque, int64_t offset, int whence))
{
    AVIOContext *s = av_mallocz(sizeof(AVIOContext));//申請AVIOContext內存
    if (!s)
        return NULL;
	//把傳進來的參數(函數指針)指向剛創建的AVIOContext結構體對應的變量(函數指針)中
    ffio_init_context(s, buffer, buffer_size, write_flag, opaque,
                  read_packet, write_packet, seek);
    return s;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章