From b14d8f5ee1eaf9bf96fbf43df91b4d0a481e68a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Wed, 6 May 2026 22:33:53 +0200 Subject: [PATCH] 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. --- demux/demux_lavf.c | 44 +++++++++++- stream/stream_curl.c | 157 +++++++++++++++++++++++++++++++++++++++++++ stream/stream_curl.h | 21 ++++++ 3 files changed, 221 insertions(+), 1 deletion(-) diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index 719d0653bb..7e3ca8376f 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -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"); diff --git a/stream/stream_curl.c b/stream/stream_curl.c index 2a146f0c02..61b2c259d4 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -22,6 +22,11 @@ #include +#include +#include +#include +#include + #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); + } +} diff --git a/stream/stream_curl.h b/stream/stream_curl.h index 10750819b6..6e5016dc14 100644 --- a/stream/stream_curl.h +++ b/stream/stream_curl.h @@ -21,7 +21,28 @@ #include "config.h" +#include + 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);