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 -0300avcodec/container_fifo: add missing stddef.h includeFixes make checkheadersSigned-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开头。
解决
所以,解决的办法也出来了,有几个:
- 更改FFmpeg的源码,在protocol_list.c的protocol_list中,加入rtsp的协议。
- 不使用avio_open这个函数,自己实现一个打开AVIOContext的函数,在这个函数中,打开rtsp服务器的连接。
- 给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的问题了。