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:
Kacper Michajłow committed 2026-05-16 18:20:42 +02:00
1 parent f658223abb
commit b14d8f5ee1
3 files changed
+221 -1

No files matched your search

+43 -1
View File
@@ -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");
+157
View File
@@ -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
View File
@@ -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);