demux_lavf: route nested HTTP opens through the curl backend
Hook AVFormatContext.io_open so HLS/DASH segment fetches and other nested FFmpeg I/O go through stream_http when libcurl is available. This lets segments inherit the connection reuse, HTTP/2 multiplexing, mid-stream resume and conditional compression of the top-level stream. Falls back to FFmpeg's default I/O for non-HTTP URLs and when the curl backend is not built in.
This commit is contained in:
1 parent
f658223abb
commit
b14d8f5ee1
3 files changed
+221
-1
No files matched your search
+43
-1
@@ -49,6 +49,8 @@
|
||||
#include "misc/thread_tools.h"
|
||||
|
||||
#include "stream/stream.h"
|
||||
#include "stream/stream_curl.h"
|
||||
|
||||
#include "demux.h"
|
||||
#include "stheader.h"
|
||||
#include "options/m_config.h"
|
||||
@@ -214,6 +216,7 @@ static const struct format_hack format_hacks[] = {
|
||||
struct nested_stream {
|
||||
AVIOContext *id;
|
||||
int64_t last_bytes;
|
||||
void *curl_data;
|
||||
};
|
||||
|
||||
struct stream_info {
|
||||
@@ -945,12 +948,24 @@ static int nested_io_open(struct AVFormatContext *s, AVIOContext **pb,
|
||||
}
|
||||
}
|
||||
|
||||
int r = priv->default_io_open(s, pb, url, flags, options);
|
||||
// Try the libcurl-based backend first, so nested connections use the same
|
||||
// stack. Only ENOSYS (the URL not supported) falls back to Lavf IO.
|
||||
void *curl_data = NULL;
|
||||
int r = AVERROR(ENOSYS);
|
||||
#if HAVE_LIBCURL
|
||||
r = mp_curl_avio_open(demuxer, pb, &curl_data, url, flags, options,
|
||||
s->protocol_whitelist, s->protocol_blacklist);
|
||||
mp_assert(r != 0 || curl_data != NULL);
|
||||
#endif
|
||||
if (r == AVERROR(ENOSYS))
|
||||
r = priv->default_io_open(s, pb, url, flags, options);
|
||||
|
||||
if (r >= 0) {
|
||||
if (options)
|
||||
mp_avdict_print_unset(demuxer->log, MSGL_TRACE, *options);
|
||||
struct nested_stream nest = {
|
||||
.id = *pb,
|
||||
.curl_data = curl_data,
|
||||
};
|
||||
MP_TARRAY_APPEND(priv, priv->nested, priv->num_nested, nest);
|
||||
}
|
||||
@@ -963,13 +978,22 @@ static int nested_io_close2(struct AVFormatContext *s, AVIOContext *pb)
|
||||
mp_require(demuxer);
|
||||
lavf_priv_t *priv = demuxer->priv;
|
||||
|
||||
MP_UNUSED void *curl_data = NULL;
|
||||
for (int n = 0; n < priv->num_nested; n++) {
|
||||
if (priv->nested[n].id == pb) {
|
||||
curl_data = priv->nested[n].curl_data;
|
||||
MP_TARRAY_REMOVE_AT(priv->nested, priv->num_nested, n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_LIBCURL
|
||||
if (curl_data) {
|
||||
mp_curl_avio_close(pb, curl_data);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
return priv->default_io_close2(s, pb);
|
||||
}
|
||||
|
||||
@@ -1377,6 +1401,22 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
|
||||
avfc->io_open = block_io_open;
|
||||
}
|
||||
|
||||
#if HAVE_LIBCURL
|
||||
// When nested HTTP requests are routed through our libcurl backend,
|
||||
// Lavf's HLS demuxer must not try to reuse the URLContext of the previous
|
||||
// request via ff_http_do_new_request2(). Our AVIOContext is not backed by a
|
||||
// URLContext, so ffio_geturlcontext() returns NULL and av_assert0() trips.
|
||||
// Note that our implementation will reuse and multiplex connections.
|
||||
// This will be fixed upstream, but keep compatibility with older versions.
|
||||
if (demuxer->access_references) {
|
||||
av_dict_set(&dopts, "http_persistent", "0", 0);
|
||||
// Actually enable http_multiple, this is basic prefetching logic in
|
||||
// HLS demuxer. We just need to avoid autodetection (-1) which would
|
||||
// be incorrect as our backed is handling everything.
|
||||
av_dict_set(&dopts, "http_multiple", "1", 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
mp_set_avdict(&dopts, lavfdopts->avopts);
|
||||
|
||||
if (av_dict_copy(&priv->av_opts, dopts, 0) < 0) {
|
||||
@@ -1533,6 +1573,8 @@ static bool demux_lavf_read_packet(struct demuxer *demux,
|
||||
av_packet_free(&pkt);
|
||||
if (r == AVERROR_EOF)
|
||||
return false;
|
||||
if (mp_cancel_test(demux->cancel))
|
||||
return false;
|
||||
MP_WARN(demux, "error reading packet: %s.\n", av_err2str(r));
|
||||
if (priv->retry_counter >= 10) {
|
||||
MP_ERR(demux, "...treating it as fatal error.\n");
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <libavformat/avio.h>
|
||||
#include <libavutil/avstring.h>
|
||||
#include <libavutil/error.h>
|
||||
#include <libavutil/mem.h>
|
||||
|
||||
#include "stream.h"
|
||||
#include "stream_curl.h"
|
||||
|
||||
@@ -29,8 +34,10 @@
|
||||
#include "common/global.h"
|
||||
#include "common/msg.h"
|
||||
#include "cookies.h"
|
||||
#include "demux/demux.h"
|
||||
#include "misc/bstr.h"
|
||||
#include "misc/dispatch.h"
|
||||
#include "misc/path_utils.h"
|
||||
#include "misc/thread_tools.h"
|
||||
#include "mpv_talloc.h"
|
||||
#include "network.h"
|
||||
@@ -822,3 +829,153 @@ const stream_info_t stream_info_curl = {
|
||||
.protocols = (const char *const[]){"http", "https", NULL},
|
||||
.stream_origin = STREAM_ORIGIN_NET,
|
||||
};
|
||||
|
||||
// FFmpeg AVIOContext implementation
|
||||
// Allows demuxers to use our stream_curl in nested io and sub-demuxers. This
|
||||
// should route all traffic through our implementation.
|
||||
|
||||
struct curl_avio_cookie {
|
||||
struct stream *stream;
|
||||
struct mp_cancel *cancel;
|
||||
};
|
||||
|
||||
static bool is_protocol_allowed(struct mp_log *log, const char *scheme,
|
||||
const char *whitelist, const char *blacklist)
|
||||
{
|
||||
if (whitelist && av_match_list(scheme, whitelist, ',') <= 0) {
|
||||
mp_err(log, "Protocol '%s' not on whitelist '%s'!\n", scheme, whitelist);
|
||||
return false;
|
||||
}
|
||||
if (blacklist && av_match_list(scheme, blacklist, ',') > 0) {
|
||||
mp_err(log, "Protocol '%s' on blacklist '%s'!\n", scheme, blacklist);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int curl_avio_read(void *opaque, uint8_t *buf, int size)
|
||||
{
|
||||
struct curl_avio_cookie *c = opaque;
|
||||
int ret = stream_read_partial(c->stream, buf, size);
|
||||
return ret > 0 ? ret : AVERROR_EOF;
|
||||
}
|
||||
|
||||
static int64_t curl_avio_seek(void *opaque, int64_t pos, int whence)
|
||||
{
|
||||
struct curl_avio_cookie *c = opaque;
|
||||
if (whence == AVSEEK_SIZE) {
|
||||
int64_t end = stream_get_size(c->stream);
|
||||
return end >= 0 ? end : AVERROR(ENOSYS);
|
||||
}
|
||||
if (whence == SEEK_END) {
|
||||
int64_t end = stream_get_size(c->stream);
|
||||
if (end < 0)
|
||||
return AVERROR(EINVAL);
|
||||
pos += end;
|
||||
} else if (whence == SEEK_CUR) {
|
||||
pos += stream_tell(c->stream);
|
||||
} else if (whence != SEEK_SET) {
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
if (pos < 0)
|
||||
return AVERROR(EINVAL);
|
||||
if (!stream_seek(c->stream, pos))
|
||||
return AVERROR(EIO);
|
||||
return pos;
|
||||
}
|
||||
|
||||
int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out,
|
||||
void **cookie_out, const char *url, int flags,
|
||||
AVDictionary **options,
|
||||
const char *whitelist, const char *blacklist)
|
||||
{
|
||||
*pb_out = NULL;
|
||||
*cookie_out = NULL;
|
||||
|
||||
if (flags & AVIO_FLAG_WRITE)
|
||||
return AVERROR(ENOSYS);
|
||||
|
||||
// Check protocol early, to return ENOSYS and allow lavf to fallback.
|
||||
bstr scheme = mp_split_proto(bstr0(url), NULL);
|
||||
if (!bstr_in_list0(scheme, (char **)stream_info_curl.protocols))
|
||||
return AVERROR(ENOSYS);
|
||||
|
||||
// The context is required to be initialized in global.
|
||||
mp_require(demuxer->global && demuxer->global->curl);
|
||||
|
||||
// Nested IO plumbs whitelist/blacklist through the AVDictionary, use that
|
||||
// if set, same as FFmpeg's implementation.
|
||||
if (options && *options) {
|
||||
AVDictionaryEntry *e;
|
||||
if ((e = av_dict_get(*options, "protocol_whitelist", NULL, 0)))
|
||||
whitelist = e->value;
|
||||
if ((e = av_dict_get(*options, "protocol_blacklist", NULL, 0)))
|
||||
blacklist = e->value;
|
||||
}
|
||||
|
||||
if (!is_protocol_allowed(demuxer->log, bstrdup0(demuxer, scheme), whitelist, blacklist))
|
||||
return AVERROR(EINVAL);
|
||||
|
||||
// Each nested stream gets its own mp_cancel slaved to the main demuxer,
|
||||
// so the http backend can install its own wake-up callback without
|
||||
// clobbering the top-level stream or any sibling nested stream.
|
||||
struct mp_cancel *cancel = mp_cancel_new(NULL);
|
||||
mp_cancel_set_parent(cancel, demuxer->cancel);
|
||||
|
||||
struct stream_open_args args = {
|
||||
.global = demuxer->global,
|
||||
.cancel = cancel,
|
||||
.url = url,
|
||||
.flags = STREAM_READ | (demuxer->stream_origin & STREAM_ORIGIN_MASK),
|
||||
.sinfo = &stream_info_curl,
|
||||
};
|
||||
|
||||
struct stream *s = NULL;
|
||||
int r = stream_create_with_args(&args, &s);
|
||||
if (r != STREAM_OK || !s) {
|
||||
talloc_free(cancel);
|
||||
return AVERROR(EIO);
|
||||
}
|
||||
|
||||
struct curl_avio_cookie *c = talloc_zero(NULL, struct curl_avio_cookie);
|
||||
c->stream = s;
|
||||
c->cancel = cancel;
|
||||
|
||||
void *buffer = av_malloc(64 * 1024);
|
||||
if (!buffer) {
|
||||
free_stream(s);
|
||||
talloc_free(cancel);
|
||||
talloc_free(c);
|
||||
return AVERROR(ENOMEM);
|
||||
}
|
||||
|
||||
AVIOContext *pb = avio_alloc_context(buffer, 64 * 1024, 0, c,
|
||||
curl_avio_read, NULL,
|
||||
s->seekable ? curl_avio_seek : NULL);
|
||||
if (!pb) {
|
||||
av_free(buffer);
|
||||
free_stream(s);
|
||||
talloc_free(cancel);
|
||||
talloc_free(c);
|
||||
return AVERROR(ENOMEM);
|
||||
}
|
||||
pb->seekable = s->seekable ? AVIO_SEEKABLE_NORMAL : 0;
|
||||
|
||||
*pb_out = pb;
|
||||
*cookie_out = c;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mp_curl_avio_close(AVIOContext *pb, void *cookie)
|
||||
{
|
||||
struct curl_avio_cookie *c = cookie;
|
||||
if (pb) {
|
||||
av_freep(&pb->buffer);
|
||||
avio_context_free(&pb);
|
||||
}
|
||||
if (c) {
|
||||
free_stream(c->stream);
|
||||
talloc_free(c->cancel);
|
||||
talloc_free(c);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,28 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libavformat/avio.h>
|
||||
|
||||
struct mpv_global;
|
||||
struct demuxer;
|
||||
|
||||
// Initialize libcurl state, must be called before stream_curl is used.
|
||||
void mp_curl_global_init(struct mpv_global *global);
|
||||
|
||||
// Open `url` via mpv's libcurl backend and wrap it as a fresh AVIOContext.
|
||||
// On success returns 0, fills *pb_out with the new context, and sets *data to
|
||||
// an opaque handle that must later be passed to mp_curl_avio_close() to
|
||||
// release all associated resources.
|
||||
// Returns AVERROR(ENOSYS) when this backend doesn't handle the URL.
|
||||
// Returns AVERROR(EINVAL) when the URL's protocol is rejected by
|
||||
// `whitelist`/`blacklist`.
|
||||
// `flags` is the AVIO_FLAG_* mask passed to AVFormatContext.io_open.
|
||||
// If `options` has `protocol_whitelist` or `protocol_blacklist` entries they
|
||||
// override the explicit `whitelist`/`blacklist` arguments (same as FFmpeg).
|
||||
int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out,
|
||||
void **data, const char *url, int flags,
|
||||
AVDictionary **options,
|
||||
const char *whitelist, const char *blacklist);
|
||||
|
||||
// Tear down an AVIOContext previously produced by mp_curl_avio_open().
|
||||
void mp_curl_avio_close(AVIOContext *pb, void *data);
|
||||
Reference in new issue
Block a user