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开头。
解决
所以,解决的办法也出来了,有几个:
- 更改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的问题了。