Protocol not found

近日,在C++中使用FFmpeg把一些本地的视频文件,推送到远程RTSP服务器的时候,使用了如下这个过程:

  • avformat_alloc_output_context2() 申请上下文
  • avcodec_find_encoder 找到编码器
  • avcodec_alloc_context3 通过找到的编码器,申请编码器上下文,设置编码器上下文
  • avcodec_open2 打开编码器
  • avformat_new_stream 新建媒体流,设置媒体流
  • avio_open 打开推送媒体的网络IO
  • avformat_write_header 写入媒体头
  • av_interleaved_frame …… 依次写入视频帧

前面的过程都一切正常,但是到了上面倒数第二步,即avio_open的时候,怎是失败,错误信息是:Protocol not found。

FFmpeg源代码

网络上已有的信息,有的说是版本问题,有的说是因为相关协议没有注册,但是都不解决问题。

于是,从FFmpeg的地址上,克隆了一份源代码,读了一下,发现了问题根源。

我们以这个版本为准,这个版本提交信息为:

commit 9d15fe77e33b757c75a4186fa049857462737713
Author: James Almer <jamrial@gmail.com>
Date:   Wed Aug 21 15:12:46 2024 -0300

    avcodec/container_fifo: add missing stddef.h include
    
    Fixes make checkheaders
    
    Signed-off-by: James Almer <jamrial@gmail.com>

提交于2024年8月21日。

我们在源代码根目录下的libavformat目录中的avio.c中的第497行,找到avio_open,发现它的定义为:

int avio_open(AVIOContext **s, const char *filename, int flags)  
{  
    return avio_open2(s, filename, flags, NULL, NULL);  
}

而avio_open2的定义为:

int avio_open2(AVIOContext **s, const char *filename, int flags,  
               const AVIOInterruptCB *int_cb, AVDictionary **options)  
{  
    return ffio_open_whitelist(s, filename, flags, int_cb, options, NULL, NULL);  
}

即,avio_open的定义实际上为ffio_open_whitelist。

而avio.c的471行就是ffio_open_whitelist:

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;  
  
    *s = NULL;  
  
    err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);  
    if (err < 0)  
        return err;  
    err = ffio_fdopen(s, h);  
    if (err < 0) {  
        ffurl_close(h);  
        return err;  
    }  
    return 0;  
}

我们看到,ffio_open_whitelist首先调用了ffurl_open_whitelist,我们先看这个函数。

ffurl_open_whitelist的定义在avio.c的362行:

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;  
    int ret = ffurl_alloc(puc, filename, flags, int_cb);  
    if (ret < 0)  
        return ret;  
    if (parent) {  
        ret = av_opt_copy(*puc, parent);  
        if (ret < 0)  
            goto fail;  
    }
    ……

我们看到,又是先调用了ffurl_alloc。通过函数名称,我们大胆猜测,这应该是一个根据文件名来创建URL结构的函数,而URL中有一个关键字段就是协议,即Protocol,所以这个函数非常可能跟“Protocol not found”有关。

果然,我们在avio.c的349行,看到这个函数的定义:

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

最后,如果p == NULL,则*pub会被设置为NULL,之后返回AVERROR_PROTOCOL_NOT_FOUND。

而AVERROR_PROTOCOL_NOT_FOUND在libavutil/error.h中,是一个宏,最后注释文字为:Protocol not found。

#define AVERROR_PROTOCOL_NOT_FOUND FFERRTAG(0xF8,'P','R','O') ///< Protocol not found

我们看一下url_find_protocol在什么情况下会返回NULL。

url_find_protocol的实现为:

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根据:截取出来,做为协议名,之后就用ffurl_get_protocols取得的数组依次对比,如果没有相等的,就会返回NULL。

而ffurl_get_protocols定义的libavformat/protocols.c中,这个函数的返回值直接复制自一个数组url_protocols。

const URLProtocol **ffurl_get_protocols(const char *whitelist,  
                                        const char *blacklist)  
{  
    const URLProtocol **ret;  
    int i, ret_idx = 0;  
  
    ret = av_calloc(FF_ARRAY_ELEMS(url_protocols), sizeof(*ret));  
    if (!ret)  
        return NULL;  
  
    for (i = 0; url_protocols[i]; i++) {  
        const URLProtocol *up = url_protocols[i];  
  
        if (whitelist && *whitelist && !av_match_name(up->name, whitelist))  
            continue;  
        if (blacklist && *blacklist && av_match_name(up->name, blacklist))  
            continue;  
  
        ret[ret_idx++] = up;  
    }  
  
    return ret;  
}

而url_protocols的定义在libavformat/protocol_list.c中,是一个数组:

static const URLProtocol * const url_protocols[] = {  
    &ff_async_protocol,  
    &ff_cache_protocol,  
    &ff_concat_protocol,  
    &ff_concatf_protocol,  
    &ff_crypto_protocol,  
    &ff_data_protocol,  
    &ff_fd_protocol,  
    &ff_ffrtmphttp_protocol,  
    &ff_file_protocol,  
    &ff_ftp_protocol,  
    &ff_gopher_protocol,  
    &ff_hls_protocol,  
    &ff_http_protocol,  
    &ff_httpproxy_protocol,  
    &ff_icecast_protocol,  
    &ff_mmsh_protocol,  
    &ff_mmst_protocol,  
    &ff_md5_protocol,  
    &ff_pipe_protocol,  
    &ff_prompeg_protocol,  
    &ff_rtmp_protocol,  
    &ff_rtmpt_protocol,  
    &ff_rtp_protocol,  
    &ff_srtp_protocol,  
    &ff_subfile_protocol,  
    &ff_tee_protocol,  
    &ff_tcp_protocol,  
    &ff_udp_protocol,  
    &ff_udplite_protocol,  
    &ff_unix_protocol,  
    NULL };

这其中,根本就没有一个协议的名称会以rtsp开头。

解决

所以,解决的办法也出来了,有几个:

  1. 更改FFmpeg的源码,在protocol_list.c的protocol_list中,加入rtsp的协议。
  2. 不使用avio_open这个函数,自己实现一个打开AVIOContext的函数,在这个函数中,打开rtsp服务器的连接。
  3. 给avio_open传入一个不是rtsp协议的文件名,但是可以打开到rtsp服务器的连接。

其中,第一个最彻底,但是需要改变宿主系统上的FFmpeg,这个有时候不是很方便。

第二个可以实现,但是稍微麻烦一点。

第三个最简单,RTSP协议走的就是TCP协议,而protocol_list中就包括TCP。

所以,我使用了:

int my_avio_open(AVFormatContext *fmt, const string &uri) {
  auto real_uri = uri;  
  auto pos = uri.find (':');  
  if (pos == string::npos)  
    {  
      _warning ("uri '%s' error", uri.c_str ());  
      return -1;  
    }  
  real_uri = string ("tcp") + uri.substr (pos);  
  s = avio_open (&fmt->pb, real_uri.c_str (), AVIO_FLAG_WRITE);
  return 0;
  }

就暂时性地算是丑陋地解决了Protocol not found的问题了。

09-12 07:40