From 8804d1739260cbd171edfe3b44c822f3b0c85a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 9 May 2026 10:05:33 +0200 Subject: [PATCH 01/17] stream/cookies: allow reading cookies not only from a local file --- stream/cookies.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stream/cookies.c b/stream/cookies.c index 2543f055ae91e..13b6f97aad433 100644 --- a/stream/cookies.c +++ b/stream/cookies.c @@ -81,7 +81,9 @@ static struct cookie_list_type *load_cookies_from(void *ctx, const char *filename) { mp_verbose(log, "Loading cookie file: %s\n", filename); - bstr data = stream_read_file(filename, ctx, global, 1000000); + bstr data = stream_read_file2(filename, ctx, + STREAM_READ_FILE_FLAGS_DEFAULT & ~STREAM_LOCAL_FS_ONLY, + global, 1000000); if (!data.start) { mp_verbose(log, "Error reading\n"); return NULL; From 4bbb114c599cd5cf6f0c8796d00d43c924cb801d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 8 May 2026 21:30:49 +0200 Subject: [PATCH 02/17] ytdl_hook.lua: write cookies via cookies-file instead of stream-lavf-o stream-lavf-o[cookies] only feeds the lavf stream backend. Instead of side-loading it, use core `--cookies-file` option. --- player/lua/ytdl_hook.lua | 50 +++++++++++++++------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/player/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua index 2f2e6309470b8..7f5fbcfd481e4 100644 --- a/player/lua/ytdl_hook.lua +++ b/player/lua/ytdl_hook.lua @@ -193,40 +193,33 @@ local function parse_cookies(cookies_line) return cookies end --- serialize cookies for avformat -local function serialize_cookies_for_avformat(cookies) +-- serialize cookies in Netscape cookies.txt format +local function serialize_cookies(cookies) local result = '' for _, cookie in pairs(cookies) do - local cookie_str = ('%s=%s; '):format(cookie.name, cookie.value:gsub('^"(.+)"$', '%1')) - for k, v in pairs(cookie) do - if k ~= "name" and k ~= "value" then - cookie_str = cookie_str .. ('%s=%s; '):format(k, v) - end - end - result = result .. cookie_str .. '\r\n' + local domain = cookie.domain or "" + result = result .. ("%s\t%s\t%s\t%s\t%s\t%s\t%s\n"):format( + domain, + (domain:sub(1, 1) == ".") and "TRUE" or "FALSE", + cookie.path or "/", + cookie.secure and "TRUE" or "FALSE", + cookie.expires or "0", + cookie.name, + cookie.value:gsub('^"(.+)"$', '%1')) end return result end --- set file-local cookies, preserving existing ones +-- set file-local cookies-file pointing at an in-memory cookies.txt local function set_cookies(cookies) if not cookies or cookies == "" then return end - - local option_key = "file-local-options/stream-lavf-o" - local stream_opts = mp.get_property_native(option_key, {}) - local existing_cookies = parse_cookies(stream_opts["cookies"]) - - local new_cookies = parse_cookies(cookies) - for cookie_key, cookie in pairs(new_cookies) do - if not existing_cookies[cookie_key] then - existing_cookies[cookie_key] = cookie - end + local data = serialize_cookies(parse_cookies(cookies)) + if data ~= "" then + mp.set_property_bool("file-local-options/cookies", true) + mp.set_property("file-local-options/cookies-file", "memory://" .. data) end - - stream_opts["cookies"] = serialize_cookies_for_avformat(existing_cookies) - mp.set_property_native(option_key, stream_opts) end local function append_libav_opt(props, name, value) @@ -854,14 +847,7 @@ local function add_single_video(json) "http_proxy", json.proxy) end - if cookies and cookies ~= "" then - local existing_cookies = parse_cookies(stream_opts["cookies"]) - local new_cookies = parse_cookies(cookies) - for cookie_key, cookie in pairs(new_cookies) do - existing_cookies[cookie_key] = cookie - end - stream_opts["cookies"] = serialize_cookies_for_avformat(existing_cookies) - end + set_cookies(cookies) local chunk_size = math.huge if has_requested_formats then @@ -1061,7 +1047,7 @@ local function run_ytdl_hook(url) -- can't change the http headers for each entry, so use the 1st set_http_headers(json.entries[1].http_headers) - set_cookies(json.entries[1].cookies or json.cookies) + playlist_cookies[playlist] = json.entries[1].cookies or json.cookies mp.set_property("stream-open-filename", playlist) if json.title and mp.get_property("force-media-title", "") == "" then From 9b21dffecbf8f6b86edc8d17441919e1a694efb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Wed, 6 May 2026 23:10:37 +0200 Subject: [PATCH 03/17] options: fix stream_lavf options struct name --- options/options.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/options/options.h b/options/options.h index a8950df4a2920..708964022fc69 100644 --- a/options/options.h +++ b/options/options.h @@ -369,7 +369,7 @@ typedef struct MPOpts { struct mp_bluray_opts *stream_bluray_opts; struct cdda_opts *stream_cdda_opts; struct dvb_opts *stream_dvb_opts; - struct lavf_opts *stream_lavf_opts; + struct stream_lavf_opts *stream_lavf_opts; struct demux_rawaudio_opts *demux_rawaudio; struct demux_rawvideo_opts *demux_rawvideo; From 1ee1c6f160e717d8214de17d883e4003dd6f3a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Wed, 6 May 2026 23:17:06 +0200 Subject: [PATCH 04/17] stream: split shared network options into their own sub-options Will be useful for future commits --- meson.build | 1 + options/options.c | 2 ++ options/options.h | 1 + stream/network.c | 45 ++++++++++++++++++++++++++++++++++++++++++++ stream/network.h | 38 +++++++++++++++++++++++++++++++++++++ stream/stream_lavf.c | 42 ++++++++++------------------------------- 6 files changed, 97 insertions(+), 32 deletions(-) create mode 100644 stream/network.c create mode 100644 stream/network.h diff --git a/meson.build b/meson.build index ab09af61177a8..d6b74ea06adbf 100644 --- a/meson.build +++ b/meson.build @@ -184,6 +184,7 @@ sources = files( ## Streams 'stream/cookies.c', + 'stream/network.c', 'stream/stream.c', 'stream/stream_avdevice.c', 'stream/stream_cb.c', diff --git a/options/options.c b/options/options.c index 66a5a6f7268f9..a64e3e9f6f1f7 100644 --- a/options/options.c +++ b/options/options.c @@ -61,6 +61,7 @@ extern const struct m_sub_options tv_params_conf; extern const struct m_sub_options stream_bluray_conf; extern const struct m_sub_options stream_cdda_conf; extern const struct m_sub_options stream_dvb_conf; +extern const struct m_sub_options mp_network_conf; extern const struct m_sub_options stream_lavf_conf; extern const struct m_sub_options sws_conf; extern const struct m_sub_options zimg_conf; @@ -672,6 +673,7 @@ static const m_option_t mp_opts[] = { #if HAVE_DVBIN {"dvbin", OPT_SUBSTRUCT(stream_dvb_opts, stream_dvb_conf)}, #endif + {"", OPT_SUBSTRUCT(network_opts, mp_network_conf)}, {"", OPT_SUBSTRUCT(stream_lavf_opts, stream_lavf_conf)}, // ------------------------- a-v sync options -------------------- diff --git a/options/options.h b/options/options.h index 708964022fc69..9275f8c259827 100644 --- a/options/options.h +++ b/options/options.h @@ -369,6 +369,7 @@ typedef struct MPOpts { struct mp_bluray_opts *stream_bluray_opts; struct cdda_opts *stream_cdda_opts; struct dvb_opts *stream_dvb_opts; + struct mp_network_opts *network_opts; struct stream_lavf_opts *stream_lavf_opts; struct demux_rawaudio_opts *demux_rawaudio; diff --git a/stream/network.c b/stream/network.c new file mode 100644 index 0000000000000..b2c7ce90f0fe7 --- /dev/null +++ b/stream/network.c @@ -0,0 +1,45 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include + +#include "network.h" +#include "options/m_option.h" + +#define OPT_BASE_STRUCT struct mp_network_opts + +const struct m_sub_options mp_network_conf = { + .opts = (const m_option_t[]) { + {"http-header-fields", OPT_STRINGLIST(http_header_fields)}, + {"user-agent", OPT_STRING(useragent)}, + {"referrer", OPT_STRING(referrer)}, + {"cookies", OPT_BOOL(cookies_enabled)}, + {"cookies-file", OPT_STRING(cookies_file), .flags = M_OPT_FILE}, + {"tls-verify", OPT_BOOL(tls_verify)}, + {"tls-ca-file", OPT_STRING(tls_ca_file), .flags = M_OPT_FILE}, + {"tls-cert-file", OPT_STRING(tls_cert_file), .flags = M_OPT_FILE}, + {"tls-key-file", OPT_STRING(tls_key_file), .flags = M_OPT_FILE}, + {"network-timeout", OPT_DOUBLE(timeout), M_RANGE(0, DBL_MAX)}, + {"http-proxy", OPT_STRING(http_proxy)}, + {0} + }, + .size = sizeof(struct mp_network_opts), + .defaults = &(const struct mp_network_opts){ + .useragent = "libmpv", + .timeout = 60, + }, +}; diff --git a/stream/network.h b/stream/network.h new file mode 100644 index 0000000000000..d7516b2e43438 --- /dev/null +++ b/stream/network.h @@ -0,0 +1,38 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#pragma once + +#include + +struct m_sub_options; + +struct mp_network_opts { + bool cookies_enabled; + char *cookies_file; + char *useragent; + char *referrer; + char **http_header_fields; + bool tls_verify; + char *tls_ca_file; + char *tls_cert_file; + char *tls_key_file; + double timeout; + char *http_proxy; +}; + +extern const struct m_sub_options mp_network_conf; diff --git a/stream/stream_lavf.c b/stream/stream_lavf.c index 7910b820820d2..868f227cc73a0 100644 --- a/stream/stream_lavf.c +++ b/stream/stream_lavf.c @@ -28,6 +28,7 @@ #include "misc/charset_conv.h" #include "misc/thread_tools.h" #include "stream.h" +#include "network.h" #include "options/m_config.h" #include "options/m_option.h" @@ -36,43 +37,18 @@ #include "misc/bstr.h" #include "mpv_talloc.h" -#define OPT_BASE_STRUCT struct stream_lavf_params -struct stream_lavf_params { +#define OPT_BASE_STRUCT struct stream_lavf_opts + +struct stream_lavf_opts { char **avopts; - bool cookies_enabled; - char *cookies_file; - char *useragent; - char *referrer; - char **http_header_fields; - bool tls_verify; - char *tls_ca_file; - char *tls_cert_file; - char *tls_key_file; - double timeout; - char *http_proxy; }; const struct m_sub_options stream_lavf_conf = { .opts = (const m_option_t[]) { {"stream-lavf-o", OPT_KEYVALUELIST(avopts)}, - {"http-header-fields", OPT_STRINGLIST(http_header_fields)}, - {"user-agent", OPT_STRING(useragent)}, - {"referrer", OPT_STRING(referrer)}, - {"cookies", OPT_BOOL(cookies_enabled)}, - {"cookies-file", OPT_STRING(cookies_file), .flags = M_OPT_FILE}, - {"tls-verify", OPT_BOOL(tls_verify)}, - {"tls-ca-file", OPT_STRING(tls_ca_file), .flags = M_OPT_FILE}, - {"tls-cert-file", OPT_STRING(tls_cert_file), .flags = M_OPT_FILE}, - {"tls-key-file", OPT_STRING(tls_key_file), .flags = M_OPT_FILE}, - {"network-timeout", OPT_DOUBLE(timeout), M_RANGE(0, DBL_MAX)}, - {"http-proxy", OPT_STRING(http_proxy)}, {0} }, - .size = sizeof(struct stream_lavf_params), - .defaults = &(const struct stream_lavf_params){ - .useragent = "libmpv", - .timeout = 60, - }, + .size = sizeof(struct stream_lavf_opts), }; static const char *const http_like[] = @@ -184,8 +160,8 @@ void mp_setup_av_network_options(AVDictionary **dict, const char *target_fmt, struct mpv_global *global, struct mp_log *log) { void *temp = talloc_new(NULL); - struct stream_lavf_params *opts = - mp_get_config_group(temp, global, &stream_lavf_conf); + struct mp_network_opts *opts = + mp_get_config_group(temp, global, &mp_network_conf); // HTTP specific options (other protocols ignore them) if (opts->useragent) @@ -239,7 +215,9 @@ void mp_setup_av_network_options(AVDictionary **dict, const char *target_fmt, if (opts->http_proxy && opts->http_proxy[0]) av_dict_set(dict, "http_proxy", opts->http_proxy, 0); - mp_set_avdict(dict, opts->avopts); + struct stream_lavf_opts *lavf_opts = + mp_get_config_group(temp, global, &stream_lavf_conf); + mp_set_avdict(dict, lavf_opts->avopts); talloc_free(temp); } From 70b3d2fdd8b0ba126af75503c4c1b113aa349cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 7 May 2026 00:37:51 +0200 Subject: [PATCH 05/17] ci: install curl --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fcf73b5d5818d..f62bc75bdeac1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -310,7 +310,7 @@ jobs: brew update fi if brew list ffmpeg &> /dev/null; then brew unlink ffmpeg; fi - brew install -q autoconf automake pkgconf libtool python freetype fribidi little-cms2 \ + brew install -q autoconf automake curl pkgconf libtool python freetype fribidi little-cms2 \ luajit libass ffmpeg-full meson rust uchardet mujs libplacebo molten-vk vulkan-loader vulkan-headers \ libarchive libbluray libcaca libcdio-paranoia libdvdnav rubberband zimg brew link ffmpeg-full @@ -439,6 +439,7 @@ jobs: apk update apk add \ binutils \ + curl-dev \ ffmpeg-dev \ gcc \ git \ @@ -493,6 +494,7 @@ jobs: run: | sudo pkg_add -U \ cmake \ + curl \ ffmpeg \ git \ libarchive \ @@ -541,6 +543,7 @@ jobs: sudo pkg install -y \ alsa-lib \ cmake \ + curl \ evdev-proto \ ffmpeg \ git \ @@ -625,6 +628,7 @@ jobs: ca-certificates:p cc:p cppwinrt:p + curl:p ffmpeg:p lcms2:p libarchive:p From 1a737a03dd0afa9fb4962d5c5c178267aaefd87e Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 9 May 2026 11:14:04 +0200 Subject: [PATCH 06/17] ci/mingw: add libcurl --- ci/build-mingw64.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ci/build-mingw64.sh b/ci/build-mingw64.sh index 1687de287b6da..c91f82ca893e0 100755 --- a/ci/build-mingw64.sh +++ b/ci/build-mingw64.sh @@ -324,6 +324,17 @@ _subrandr () { } _subrandr_mark=lib/libsubrandr.dll.a +_curl () { + local ver=8.20.0 + gettar "https://curl.se/download/curl-${ver}.tar.xz" + builddir curl-${ver} + cmake .. "${cmake_args[@]}" \ + -DCURL_{USE_SCHANNEL,ZLIB}=ON -DCURL_DISABLE_LDAP=ON -DCURL_USE_LIBPSL=OFF + makeplusinstall + popd +} +_curl_mark=lib/libcurl.dll.a + for x in iconv zlib-ng shaderc spirv-cross amf-headers nv-headers dav1d lcms2; do build_if_missing $x done @@ -331,7 +342,7 @@ if [[ "$TARGET" != "i686-"* ]]; then build_if_missing vulkan-headers build_if_missing vulkan-loader fi -for x in ffmpeg libplacebo freetype fribidi harfbuzz libass luajit; do +for x in ffmpeg libplacebo freetype fribidi harfbuzz libass luajit curl; do build_if_missing $x done if [[ "$TARGET" != "i686-"* ]]; then @@ -358,7 +369,7 @@ mpv_args=( -Dmujs:werror=false -Dmujs:default_library=static -Dlua=luajit - -D{amf,shaderc,spirv-cross,d3d11,javascript}=enabled + -D{amf,shaderc,spirv-cross,d3d11,javascript,libcurl}=enabled ) meson setup $build "${mpv_args[@]}" meson compile -C $build @@ -384,7 +395,7 @@ if [ "$2" = pack ]; then av*.dll sw*.dll postproc-[0-9]*.dll # everything else subrandr-[0-9]*.dll lib{ass,freetype,fribidi,harfbuzz,iconv,placebo}-[0-9]*.dll - lib{shaderc_shared,spirv-cross-c-shared,dav1d,lcms2,zlib1}.dll + lib{curl,shaderc_shared,spirv-cross-c-shared,dav1d,lcms2,zlib1}.dll ) [[ -f vulkan-1.dll ]] && dlls+=(vulkan-1.dll) mv -v "${dlls[@]}" .. From 5ffd22135ef8bb71bf1a4995f362703caecec6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 7 May 2026 10:35:27 +0200 Subject: [PATCH 07/17] ci/build-win32.ps1: disable libpsl tests build --- ci/build-win32.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/build-win32.ps1 b/ci/build-win32.ps1 index 4418eab43136c..d7410651f96f9 100644 --- a/ci/build-win32.ps1 +++ b/ci/build-win32.ps1 @@ -322,6 +322,7 @@ meson setup build ` -Dlibplacebo:tests=false ` -Dlibplacebo:vulkan=enabled ` -Dlibplacebo:d3d11=enabled ` + -Dlibpsl:tests=false ` -Dxxhash:inline-all=true ` -Dxxhash:cli=false ` -Dluajit:amalgam=true ` From 0e86934ef82a873bfbc120cc2881d9c96fb02fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 6 Oct 2024 17:37:04 +0200 Subject: [PATCH 08/17] stream_curl: add libcurl-based stream backend Introduces stream_http, an internal libcurl-driven backend for http:// and https:// URLs. Runs all transfers on a dedicated curl multi thread with a producer-side ring buffer, and exposes HTTP/2 multiplexing, QUIC/HTTP/3 (when libcurl supports it), HSTS and TCP keep-alive. Should generally be more stable and faster than FFmpeg HTTP/1.1 impl, additionally connections are kept-alive between files open, so if you open playlist of network files and navigate through them, it will re-use the same connection. Build is gated on the new 'libcurl' meson option (auto). When disabled or unavailable, mpv silently falls back to FFmpeg's HTTP implementation. --- DOCS/interface-changes/http-backend.txt | 1 + DOCS/man/options.rst | 61 ++ common/global.h | 1 + meson.build | 7 + meson.options | 1 + options/options.c | 5 + options/options.h | 2 + player/main.c | 3 + stream/stream.c | 4 + stream/stream_curl.c | 813 ++++++++++++++++++++++++ stream/stream_curl.h | 31 + 11 files changed, 929 insertions(+) create mode 100644 DOCS/interface-changes/http-backend.txt create mode 100644 stream/stream_curl.c create mode 100644 stream/stream_curl.h diff --git a/DOCS/interface-changes/http-backend.txt b/DOCS/interface-changes/http-backend.txt new file mode 100644 index 0000000000000..291cccaae3cee --- /dev/null +++ b/DOCS/interface-changes/http-backend.txt @@ -0,0 +1 @@ +add libcurl-based stream backend, with new options `--curl-http-version`, `--curl-max-redirects`, `--curl-max-retries`, `--curl-connect-timeout`, `--curl-buffer-size` and `--curl-max-request-size` diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index ed4eb7e4e8879..a13fda9ce4ffc 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -5718,6 +5718,67 @@ Network The bitrate as used is sent by the server, and there's no guarantee it's actually meaningful. +HTTP backend (libcurl) +---------------------- + +When mpv is built with libcurl support, ``http://`` and ``https://`` URLs are +served by an internal libcurl-based stream backend instead of FFmpeg. The +backend fully supports all features of libcurl, making it more robust and +compatible with a wide range of servers and CDNs, and faster too. + +For HTTP transfers, the backend transparently negotiates HTTP/1.1, HTTP/2 +multiplexing or HTTP/3 (QUIC) when the server offers them, with HSTS enabled +and TCP keep-alive turned on. Content compression (gzip, deflate, zstd, +brotli) is always advertised in the request. If the server applies it, the +transfer is treated as non-seekable. Servers normally do not compress +already-compressed media payloads. Otherwise, it's great improvement for text +playlist data transfers. + +The backend honors the network options listed above (``--user-agent``, +``--http-proxy``, ``--http-header-fields``, ``--referrer``, ``--cookies*``, +``--tls-*``). + +If libcurl is not available at build time, or if this backend fails to handle +a particular request, mpv falls back to FFmpeg's HTTP implementation. + +To inspect libcurl's debug output (requests, response headers, +TLS/connection diagnostics), set ``--msg-level=curl=trace``. + +``--curl-http-version=`` + Select the maximum HTTP protocol version libcurl is allowed to negotiate. + If libcurl was built without HTTP/3 support, it will fallback to ``auto``. + (default: ``auto``, i.e. let libcurl pick) + +``--curl-max-redirects=<0-100>`` + Maximum number of HTTP redirects to follow before reporting an error + (default: 16). + +``--curl-max-retries=<0-100>`` + Number of times a single seekable transfer may be transparently + re-attempted after a recoverable error (timeout, connection drop, + HTTP/2 stream reset, ...) before the stream gives up (default: 5). + Non-seekable transfers cannot be retried and ignore this option. + +``--curl-connect-timeout=`` + TCP/TLS connect timeout in seconds (default: 30, range 0-600). 0 lets + libcurl use its built-in default. The overall transfer timeout is + controlled by ``--network-timeout``. + +``--curl-buffer-size=`` + Size of the per-stream producer-side ring buffer that decouples the + network thread from the consumer (default: 4 MiB, minimum: 32 KiB). + Lower values may reduce in-flight data and reduce latency. + +``--curl-max-request-size=`` + For seekable streams, split the transfer into Range requests of at most + this size (default: 0, i.e. one open-ended request for the whole stream). + A non-zero value can help with very long-running connections that some + CDNs or proxies recycle aggressively, and is also a common workaround for + per-connection bandwidth throttling employed by some CDNs (notably some + video hosting services), where each individual Range request is served at + full speed but a single long-lived connection is rate-limited. Ignored for + non-seekable streams. + DVB --- diff --git a/common/global.h b/common/global.h index c05b70ec1adff..6cdbea552df3a 100644 --- a/common/global.h +++ b/common/global.h @@ -11,6 +11,7 @@ struct mpv_global { char *configdir; struct stats_base *stats; struct demux_packet_pool *packet_pool; + struct curl_ctx *curl; }; #endif diff --git a/meson.build b/meson.build index d6b74ea06adbf..65a9ed320e741 100644 --- a/meson.build +++ b/meson.build @@ -621,6 +621,13 @@ if darwin subdir(join_paths('TOOLS', 'osxbundle')) endif +libcurl = dependency('libcurl', version: '>= 7.83.0', required: get_option('libcurl')) +features += {'libcurl': libcurl.found()} +if features['libcurl'] + dependencies += [libcurl] + sources += files('stream/stream_curl.c') +endif + cdda_opt = get_option('cdda').require( get_option('gpl'), error_message: 'the build is not GPL!', diff --git a/meson.options b/meson.options index 836d16d03fcc4..35e174cebaa94 100644 --- a/meson.options +++ b/meson.options @@ -19,6 +19,7 @@ option('lcms2', type: 'feature', value: 'auto', description: 'LCMS2 support') option('libarchive', type: 'feature', value: 'auto', description: 'libarchive wrapper for reading zip files and more') option('libavdevice', type: 'feature', value: 'auto', description: 'libavdevice') option('libbluray', type: 'feature', value: 'auto', description: 'Bluray support') +option('libcurl', type: 'feature', value: 'auto', description: 'libcurl-based stream backend') option('lua', type: 'combo', choices: ['lua', 'lua52', 'lua5.2', 'lua-5.2', 'luajit', 'lua51', diff --git a/options/options.c b/options/options.c index a64e3e9f6f1f7..48baef13ff731 100644 --- a/options/options.c +++ b/options/options.c @@ -95,6 +95,7 @@ extern const struct m_sub_options ao_conf; extern const struct m_sub_options dvd_conf; extern const struct m_sub_options clipboard_conf; +extern const struct m_sub_options curl_conf; extern const struct m_sub_options opengl_conf; extern const struct m_sub_options vulkan_conf; @@ -926,6 +927,10 @@ static const m_option_t mp_opts[] = { {"clipboard", OPT_SUBSTRUCT(clipboard_opts, clipboard_conf)}, +#if HAVE_LIBCURL + {"curl", OPT_SUBSTRUCT(curl_opts, curl_conf)}, +#endif + {"", OPT_SUBSTRUCT(vo, vo_sub_opts)}, {"", OPT_SUBSTRUCT(demux_opts, demux_conf)}, {"", OPT_SUBSTRUCT(demux_cache_opts, demux_cache_conf)}, diff --git a/options/options.h b/options/options.h index 9275f8c259827..014e41de6d27e 100644 --- a/options/options.h +++ b/options/options.h @@ -395,6 +395,8 @@ typedef struct MPOpts { struct clipboard_opts *clipboard_opts; + struct curl_opts *curl_opts; + struct encode_opts *encode_opts; char *ipc_path; diff --git a/player/main.c b/player/main.c index 23e89005e3f60..bd9bdb547c433 100644 --- a/player/main.c +++ b/player/main.c @@ -70,6 +70,8 @@ #include "command.h" #include "screenshot.h" +#include "stream/stream_curl.h" + static const char def_config[] = #include "etc/builtin.conf.inc" ; @@ -289,6 +291,7 @@ struct MPContext *mp_create(void) demux_packet_pool_init(mpctx->global); stats_global_init(mpctx->global); + mp_curl_global_init(mpctx->global); // Nothing must call mp_msg*() and related before this mp_msg_init(mpctx->global); diff --git a/stream/stream.c b/stream/stream.c index 03dd7ba5e6baa..4a5338eb2ea7f 100644 --- a/stream/stream.c +++ b/stream/stream.c @@ -61,6 +61,7 @@ extern const stream_info_t stream_info_bluray; extern const stream_info_t stream_info_edl; extern const stream_info_t stream_info_libarchive; extern const stream_info_t stream_info_cb; +extern const stream_info_t stream_info_curl; static const stream_info_t *const stream_list[] = { &stream_info_mpv, @@ -90,6 +91,9 @@ static const stream_info_t *const stream_list[] = { &stream_info_slice, &stream_info_fd, &stream_info_cb, +#if HAVE_LIBCURL + &stream_info_curl, +#endif &stream_info_ffmpeg, &stream_info_ffmpeg_unsafe, }; diff --git a/stream/stream_curl.c b/stream/stream_curl.c new file mode 100644 index 0000000000000..6fd855ed37cf2 --- /dev/null +++ b/stream/stream_curl.c @@ -0,0 +1,813 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include +#include +#include + +#include + +#include "stream.h" +#include "stream_curl.h" + +#include "common/common.h" +#include "common/global.h" +#include "common/msg.h" +#include "cookies.h" +#include "misc/bstr.h" +#include "misc/dispatch.h" +#include "misc/thread_tools.h" +#include "mpv_talloc.h" +#include "network.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "options/path.h" +#include "osdep/threads.h" +#include "osdep/timer.h" + +struct curl_opts { + int http_version; + int max_redirects; + int max_retries; + double connect_timeout; + int64_t buffer_size; + int64_t max_request_size; +}; + +#ifndef CURL_HTTP_VERSION_3 +#define CURL_HTTP_VERSION_3 CURL_HTTP_VERSION_NONE +#endif + +#ifndef CURL_HTTP_VERSION_3ONLY +#define CURL_HTTP_VERSION_3ONLY CURL_HTTP_VERSION_NONE +#endif + +#define OPT_BASE_STRUCT struct curl_opts +const struct m_sub_options curl_conf = { + .opts = (const struct m_option[]) { + {"http-version", OPT_CHOICE(http_version, + {"auto", CURL_HTTP_VERSION_NONE}, + {"1.0", CURL_HTTP_VERSION_1_0}, + {"1.1", CURL_HTTP_VERSION_1_1}, + {"2", CURL_HTTP_VERSION_2}, + {"2tls", CURL_HTTP_VERSION_2TLS}, + {"2-prior-knowledge", CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE}, + {"3", CURL_HTTP_VERSION_3}, + {"3only", CURL_HTTP_VERSION_3ONLY} + )}, + {"max-redirects", OPT_INT(max_redirects), M_RANGE(0, 100)}, + {"max-retries", OPT_INT(max_retries), M_RANGE(0, 100)}, + {"connect-timeout", OPT_DOUBLE(connect_timeout), M_RANGE(0, 600)}, + {"buffer-size", OPT_BYTE_SIZE(buffer_size), + M_RANGE(2 * CURL_MAX_WRITE_SIZE, M_MAX_MEM_BYTES)}, + {"max-request-size", OPT_BYTE_SIZE(max_request_size), + M_RANGE(0, M_MAX_MEM_BYTES)}, + {0} + }, + .defaults = &(const struct curl_opts) { + .http_version = CURL_HTTP_VERSION_NONE, + .max_redirects = 16, + .max_retries = 5, + .connect_timeout = 30, + .buffer_size = 4 << 20, // 4 MiB + .max_request_size = 0, + }, + .size = sizeof(struct curl_opts), +}; + +struct curl_ctx { + mp_thread thread; + struct mp_dispatch_queue *dispatch; + CURLM *multi; + bool exit; +}; + +// Per-stream state, owned by the curl thread. +struct priv { + struct mp_log *log; + struct mpv_global *global; + struct curl_ctx *ctx; + struct stream *s; + + struct curl_opts *opts; + struct mp_network_opts *net_opts; + + CURL *curl; + struct curl_slist *headers; + char *url; + + // Stream parameters + bool seekable; + int64_t content_size; // -1 if unknown + + // Producer state. Only touched by the curl thread. + uint64_t request_start; // absolute byte position of next request + uint64_t request_received; // bytes received in the current request + int retry_count; // consecutive failed attempts at request_start + bool active; // handle is currently active in the multi + bool finished; // current request has reached EOF + + // Probe state. Set on the curl thread read by curl_open after. + bool probed; + bool stream_ok; + + // Shared state, protected by mtx. + mp_mutex mtx; + mp_cond cond; + uint8_t *buffer; + size_t buffer_size; + size_t head, tail, count; + bool paused; // write callback paused due to a full buffer + bool stream_eof; // producer has delivered all data + bool stream_error; // unrecoverable error + atomic_bool aborted; // canceled by user (mp_cancel) +}; + +// Curl thread + +enum cmd_kind { + CMD_ADD, + CMD_REMOVE, + CMD_SEEK, + CMD_UNPAUSE, + CMD_EXIT, +}; + +struct cmd { + enum cmd_kind kind; + struct curl_ctx *ctx; + struct priv *p; + int64_t pos; + bool drop; +}; + +static void start_request(struct priv *p); +static void on_done(struct priv *p, CURLcode code); + +static void run_cmd(void *arg) +{ + struct cmd *c = arg; + struct curl_ctx *ctx = c->ctx; + switch (c->kind) { + case CMD_ADD: + MP_TRACE(c->p, "starting curl request at %" PRIu64 "\n", c->p->request_start); + start_request(c->p); + break; + case CMD_REMOVE: + if (c->p->active) { + MP_TRACE(c->p, "removing curl handle\n"); + curl_multi_remove_handle(ctx->multi, c->p->curl); + c->p->active = false; + } + break; + case CMD_UNPAUSE: + // The consumer freed enough buffer space. Clear the pause flag and + // resume the transfer. + mp_mutex_lock(&c->p->mtx); + MP_TRACE(c->p, "resuming curl transfer\n"); + c->p->paused = false; + mp_mutex_unlock(&c->p->mtx); + curl_easy_pause(c->p->curl, CURLPAUSE_CONT); + break; + case CMD_SEEK: + MP_TRACE(c->p, "seeking to %" PRIu64 "\n", c->pos); + if (c->p->active) { + curl_multi_remove_handle(ctx->multi, c->p->curl); + c->p->active = false; + } + mp_mutex_lock(&c->p->mtx); + if (c->drop) + c->p->head = c->p->tail = c->p->count = 0; + c->p->paused = false; + c->p->stream_eof = false; + c->p->stream_error = false; + mp_cond_broadcast(&c->p->cond); + mp_mutex_unlock(&c->p->mtx); + c->p->request_start = c->pos; + c->p->request_received = 0; + c->p->retry_count = 0; + c->p->finished = false; + start_request(c->p); + break; + case CMD_EXIT: + ctx->exit = true; + break; + } +} + +static void cmd_async(struct priv *p, enum cmd_kind kind) +{ + struct cmd *c = talloc_zero(NULL, struct cmd); + c->kind = kind; + c->ctx = p->ctx; + c->p = p; + mp_dispatch_enqueue_autofree(p->ctx->dispatch, run_cmd, c); +} + +static void cmd_sync(struct priv *p, enum cmd_kind kind, int64_t pos, bool drop) +{ + struct cmd c = { + .kind = kind, + .ctx = p->ctx, + .p = p, + .pos = pos, + .drop = drop, + }; + mp_dispatch_run(p->ctx->dispatch, run_cmd, &c); +} + +static void curl_wakeup(void *arg) +{ + struct curl_ctx *ctx = arg; + curl_multi_wakeup(ctx->multi); +} + +static MP_THREAD_VOID curl_thread(void *arg) +{ + mp_thread_set_name("curl"); + struct curl_ctx *ctx = arg; + + curl_global_init(CURL_GLOBAL_ALL); + ctx->multi = curl_multi_init(); + curl_multi_setopt(ctx->multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX); + + mp_dispatch_set_wakeup_fn(ctx->dispatch, curl_wakeup, ctx); + + while (!ctx->exit) { + mp_dispatch_queue_process(ctx->dispatch, 0); + + // Stop early to avoid delays, this happens only when player is closing. + if (ctx->exit) + break; + + int running = 0; + CURLMcode mres = curl_multi_perform(ctx->multi, &running); + if (mres != CURLM_OK && mres != CURLM_CALL_MULTI_PERFORM) + break; + + CURLMsg *msg; + int left = 0; + while ((msg = curl_multi_info_read(ctx->multi, &left))) { + if (msg->msg != CURLMSG_DONE) + continue; + struct priv *p = NULL; + curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &p); + mp_assert(p); + curl_multi_remove_handle(ctx->multi, msg->easy_handle); + p->active = false; + on_done(p, msg->data.result); + } + + curl_multi_poll(ctx->multi, NULL, 0, 1000, NULL); + } + + curl_multi_cleanup(ctx->multi); + curl_global_cleanup(); + MP_THREAD_RETURN(); +} + +static void mp_curl_destroy(void *ptr) +{ + struct curl_ctx *ctx = ptr; + struct cmd c = { .kind = CMD_EXIT, .ctx = ctx }; + mp_dispatch_run(ctx->dispatch, run_cmd, &c); + mp_thread_join(ctx->thread); +} + +void mp_curl_global_init(struct mpv_global *global) +{ + struct curl_ctx *ctx = talloc_zero(global, struct curl_ctx); + talloc_set_destructor(ctx, mp_curl_destroy); + ctx->dispatch = mp_dispatch_create(ctx); + global->curl = ctx; + mp_require(!mp_thread_create(&ctx->thread, curl_thread, ctx)); +} + +// Curl callbacks + +static bool is_http_success(long resp) +{ + return resp >= 200 && resp < 300; +} + +static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + struct priv *p = userdata; + size_t bytes = size * nmemb; + + // header_callback validated the response and logged any error status, + // we don't care about error body. + if (!p->stream_ok) + return CURL_WRITEFUNC_ERROR; + + if (atomic_load_explicit(&p->aborted, memory_order_relaxed)) + return CURL_WRITEFUNC_ERROR; + + mp_mutex_lock(&p->mtx); + + if (p->buffer_size - p->count < bytes) { + // No room in the buffer. Pause the transfer and wait for the consumer. + p->paused = true; + mp_mutex_unlock(&p->mtx); + MP_TRACE(p, "pausing curl transfer, buffer full (%zu bytes)\n", p->count); + return CURL_WRITEFUNC_PAUSE; + } + + size_t tail_chunk = MPMIN(p->buffer_size - p->tail, bytes); + memcpy(p->buffer + p->tail, ptr, tail_chunk); + memcpy(p->buffer, ptr + tail_chunk, bytes - tail_chunk); + p->tail = (p->tail + bytes) % p->buffer_size; + p->count += bytes; + p->paused = false; + + p->request_received += bytes; + + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); + + return bytes; +} + +static int xferinfo_callback(void *userdata, curl_off_t dl_total, curl_off_t dl_now, + curl_off_t ul_total, curl_off_t ul_now) +{ + struct priv *p = userdata; + return atomic_load_explicit(&p->aborted, memory_order_relaxed); +} + +static int64_t parse_content_range_total(const char *value) +{ + if (!value) + return -1; + bstr after; + if (!bstr_split_tok(bstr0(value), "/", &(bstr){0}, &after)) + return -1; + bstr rest; + long long total = bstrtoll(after, &rest, 10); + return (rest.len == 0 && total > 0) ? (int64_t)total : -1; +} + +static const char *header_value(CURL *c, const char *name) +{ + struct curl_header *h = NULL; + if (curl_easy_header(c, name, 0, CURLH_HEADER, -1, &h) == CURLHE_OK) + return h->value; + return NULL; +} + +// Decide stream parameters from the header response. The stream is considered +// non-seakabke if compression is used, or server doesn't support byte ranges. +static size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) +{ + struct priv *p = userdata; + size_t bytes = size * nitems; + + if (p->probed) + return bytes; + + bstr line = bstr_strip_linebreaks((bstr){buffer, bytes}); + if (line.len > 0) + return bytes; + + long resp = 0; + curl_easy_getinfo(p->curl, CURLINFO_RESPONSE_CODE, &resp); + // Skip 1xx and intermediate 3xx (curl follows redirects internally). + if (resp < 200 || (resp >= 300 && resp < 400)) + return bytes; + + if (!is_http_success(resp)) { + MP_ERR(p, "HTTP error %ld\n", resp); + goto done; + } + + // Compressed responses are byte-addressed in the encoded representation, + // which our caller can't translate, so they are non-seekable. + const char *ce = header_value(p->curl, "Content-Encoding"); + bool compressed = ce && ce[0] && strcasecmp(ce, "identity") != 0; + const char *ar = header_value(p->curl, "Accept-Ranges"); + bool accept_ranges = ar && strcasecmp(ar, "bytes") == 0; + + // Some servers reply 200 to an open-ended "Range: 0-" but 206 to explicit + // byte ranges, so trust either. + p->seekable = !compressed && (resp == 206 || accept_ranges); + + if (p->seekable) { + // Content-Range carries the full size on a partial response. On any + // non-206 success code use Content-Length. + int64_t total = parse_content_range_total(header_value(p->curl, "Content-Range")); + if (total < 0 && resp != 206) { + curl_off_t cl = -1; + if (curl_easy_getinfo(p->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, + &cl) == CURLE_OK && cl >= 0) + total = cl; + } + p->content_size = total; + } + p->stream_ok = true; + + MP_DBG(p, "status=%ld compressed=%d size=%" PRId64 " seekable=%d\n", + resp, compressed, p->content_size, p->seekable); + +done: + mp_mutex_lock(&p->mtx); + p->probed = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); + return bytes; +} + +static int debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, + void *userdata) +{ + struct priv *p = userdata; + const char *prefix; + switch (type) { + case CURLINFO_TEXT: prefix = "* "; break; + case CURLINFO_HEADER_IN: prefix = "< "; break; + case CURLINFO_HEADER_OUT: prefix = "> "; break; + default: return 0; + } + bstr msg = bstr_strip_linebreaks((bstr){data, size}); + MP_TRACE(p, "%s%.*s\n", prefix, BSTR_P(msg)); + return 0; +} + +// Request handling + +static bool is_recoverable_error(CURLcode code) +{ + switch (code) { + case CURLE_RECV_ERROR: + case CURLE_SEND_ERROR: + case CURLE_PARTIAL_FILE: + case CURLE_OPERATION_TIMEDOUT: + case CURLE_GOT_NOTHING: + case CURLE_COULDNT_CONNECT: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_HTTP2: + case CURLE_HTTP2_STREAM: + return true; + default: + return false; + } +} + +static void start_request(struct priv *p) +{ + if (p->finished) { + mp_mutex_lock(&p->mtx); + p->stream_eof = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); + return; + } + + uint64_t start = p->request_start; + + bool ranged = !p->probed || p->seekable; + bool chunked = ranged && p->opts->max_request_size > 0; + + if (p->seekable && p->content_size > 0 && start >= p->content_size) { + p->finished = true; + mp_mutex_lock(&p->mtx); + p->stream_eof = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); + return; + } + + char range[64]; + if (chunked) { + uint64_t end = start + p->opts->max_request_size - 1; + if (p->content_size > 0) + end = MPMIN(end, p->content_size - 1); + snprintf(range, sizeof(range), "%" PRIu64 "-%" PRIu64, start, end); + curl_easy_setopt(p->curl, CURLOPT_RANGE, range); + } else if (ranged) { + snprintf(range, sizeof(range), "%" PRIu64 "-", start); + curl_easy_setopt(p->curl, CURLOPT_RANGE, range); + } else { + curl_easy_setopt(p->curl, CURLOPT_RANGE, NULL); + } + + p->request_received = 0; + p->active = true; + curl_multi_add_handle(p->ctx->multi, p->curl); +} + +static void on_done(struct priv *p, CURLcode code) +{ + bool aborted = atomic_load_explicit(&p->aborted, memory_order_relaxed); + + if (!p->probed) { + // Connection died before any headers arrived. + if (code != CURLE_OK && !aborted) + MP_ERR(p, "error: %s\n", curl_easy_strerror(code)); + mp_mutex_lock(&p->mtx); + p->probed = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); + return; + } + + // Roll completed bytes into request_start so retries and chunk + // continuations resume at the next missing byte. + p->request_start += p->request_received; + p->request_received = 0; + + if (code == CURLE_OK && !aborted) { + p->retry_count = 0; + + bool chunked = p->seekable && p->opts->max_request_size > 0; + if (chunked && (p->content_size <= 0 || p->request_start < p->content_size)) { + start_request(p); + return; + } + + p->finished = true; + mp_mutex_lock(&p->mtx); + p->stream_eof = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); + return; + } + + // Try to recover if the stream is seekable and the failure looks + // recoverable. + bool recoverable = !aborted && p->seekable && + is_recoverable_error(code) && + p->retry_count < p->opts->max_retries; + if (recoverable) { + p->retry_count++; + MP_WARN(p, "%s, retrying (#%d) from %" PRIu64 "\n", + curl_easy_strerror(code), p->retry_count, p->request_start); + start_request(p); + return; + } + + if (!aborted) + MP_ERR(p, "transfer failed: %s\n", curl_easy_strerror(code)); + + mp_mutex_lock(&p->mtx); + p->stream_error = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); +} + +static void on_cancel(void *ctx) +{ + struct priv *p = ctx; + atomic_store(&p->aborted, true); + mp_mutex_lock(&p->mtx); + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); + if (p->ctx && p->ctx->multi) + curl_multi_wakeup(p->ctx->multi); +} + +// Configuration and initial setup + +static struct curl_slist *build_header_list(struct priv *p) +{ + struct curl_slist *list = NULL; + if (p->net_opts->referrer && p->net_opts->referrer[0]) { + char *h = talloc_asprintf(NULL, "Referer: %s", p->net_opts->referrer); + list = curl_slist_append(list, h); + talloc_free(h); + } + if (p->net_opts->http_header_fields) { + for (int i = 0; p->net_opts->http_header_fields[i]; i++) + list = curl_slist_append(list, p->net_opts->http_header_fields[i]); + } + return list; +} + +static void setup_curl(struct priv *p) +{ + CURL *c = p->curl; + + curl_easy_setopt(c, CURLOPT_URL, p->url); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(c, CURLOPT_PRIVATE, p); + + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(c, CURLOPT_WRITEDATA, p); + curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(c, CURLOPT_HEADERDATA, p); + // enable progress callback, so we can cancel transfer at any point + curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(c, CURLOPT_XFERINFOFUNCTION, xferinfo_callback); + curl_easy_setopt(c, CURLOPT_XFERINFODATA, p); + + // Enable verbose output with trace level logging. + curl_easy_setopt(c, CURLOPT_VERBOSE, mp_msg_test(p->log, MSGL_TRACE) ? 1L : 0L); + curl_easy_setopt(c, CURLOPT_DEBUGFUNCTION, debug_callback); + curl_easy_setopt(c, CURLOPT_DEBUGDATA, p); + + curl_easy_setopt(c, CURLOPT_ACCEPT_ENCODING, ""); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(c, CURLOPT_MAXREDIRS, (long)p->opts->max_redirects); + curl_easy_setopt(c, CURLOPT_HTTP_VERSION, (long)p->opts->http_version); + curl_easy_setopt(c, CURLOPT_HSTS_CTRL, (long)CURLHSTS_ENABLE); + curl_easy_setopt(c, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, + (long)(p->opts->connect_timeout * 1000)); + + if (p->net_opts->useragent && p->net_opts->useragent[0]) + curl_easy_setopt(c, CURLOPT_USERAGENT, p->net_opts->useragent); + if (p->net_opts->http_proxy && p->net_opts->http_proxy[0]) + curl_easy_setopt(c, CURLOPT_PROXY, p->net_opts->http_proxy); + + curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, p->net_opts->tls_verify ? 1L : 0L); + curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, p->net_opts->tls_verify ? 2L : 0L); + if (p->net_opts->tls_ca_file) { + char *path = mp_get_user_path(p, p->global, p->net_opts->tls_ca_file); + curl_easy_setopt(c, CURLOPT_CAINFO, path); + } + if (p->net_opts->tls_cert_file) { + char *path = mp_get_user_path(p, p->global, p->net_opts->tls_cert_file); + curl_easy_setopt(c, CURLOPT_SSLCERT, path); + } + if (p->net_opts->tls_key_file) { + char *path = mp_get_user_path(p, p->global, p->net_opts->tls_key_file); + curl_easy_setopt(c, CURLOPT_SSLKEY, path); + } + + if (p->net_opts->cookies_enabled) { + char *file = p->net_opts->cookies_file; + if (file && file[0]) { + char *path = mp_get_user_path(p, p->global, file); + curl_easy_setopt(c, CURLOPT_COOKIEFILE, path); + } else { + curl_easy_setopt(c, CURLOPT_COOKIEFILE, ""); + } + } + + p->headers = build_header_list(p); + if (p->headers) + curl_easy_setopt(c, CURLOPT_HTTPHEADER, p->headers); +} + +// stream_curl implementation + +static int curl_fill_buffer(struct stream *s, void *buffer, int max_len) +{ + struct priv *p = s->priv; + if (max_len <= 0) + return 0; + + mp_mutex_lock(&p->mtx); + + while (p->count == 0 && !p->stream_eof && !p->stream_error && + !atomic_load_explicit(&p->aborted, memory_order_relaxed)) + { + mp_cond_wait(&p->cond, &p->mtx); + } + + size_t copy = MPMIN((size_t)max_len, p->count); + if (copy > 0) { + size_t head_chunk = MPMIN(p->buffer_size - p->head, copy); + memcpy(buffer, p->buffer + p->head, head_chunk); + memcpy((char *)buffer + head_chunk, p->buffer, copy - head_chunk); + p->head = (p->head + copy) % p->buffer_size; + p->count -= copy; + } + + bool unpause = p->paused && !p->stream_eof && !p->stream_error && + p->buffer_size - p->count >= p->buffer_size / 2; + + mp_mutex_unlock(&p->mtx); + + if (unpause) + cmd_async(p, CMD_UNPAUSE); + + return copy; +} + +static int curl_seek(struct stream *s, int64_t pos) +{ + struct priv *p = s->priv; + if (pos < 0) + return 0; + cmd_sync(p, CMD_SEEK, pos, true); + return 1; +} + +static int64_t curl_get_size(struct stream *s) +{ + struct priv *p = s->priv; + return p->content_size; +} + +static void priv_destructor(void *ptr) +{ + struct priv *p = ptr; + mp_cancel_set_cb(p->s->cancel, NULL, NULL); + if (p->curl) { + cmd_sync(p, CMD_REMOVE, 0, false); + curl_easy_cleanup(p->curl); + } + if (p->headers) + curl_slist_free_all(p->headers); + mp_mutex_destroy(&p->mtx); + mp_cond_destroy(&p->cond); +} + +static void curl_close(struct stream *s) +{ + struct priv *p = s->priv; + if (!p) + return; + mp_cancel_set_cb(s->cancel, NULL, NULL); + if (p->curl) { + cmd_sync(p, CMD_REMOVE, 0, false); + curl_easy_cleanup(p->curl); + p->curl = NULL; + } +} + +static int curl_open(stream_t *s, const struct stream_open_args *args) +{ + if (s->mode != STREAM_READ) + return STREAM_UNSUPPORTED; + if (!s->global || !s->global->curl) { + MP_ERR(s, "curl backend not initialized\n"); + return STREAM_ERROR; + } + + struct priv *p = talloc_zero(s, struct priv); + s->priv = p; + talloc_set_destructor(p, priv_destructor); + + p->log = s->log; + p->global = s->global; + p->ctx = s->global->curl; + p->s = s; + p->opts = mp_get_config_group(p, s->global, &curl_conf); + p->net_opts = mp_get_config_group(p, s->global, &mp_network_conf); + p->url = talloc_strdup(p, s->url); + p->content_size = -1; + p->buffer_size = p->opts->buffer_size; + p->buffer = talloc_size(p, p->buffer_size); + + mp_mutex_init(&p->mtx); + mp_cond_init(&p->cond); + p->aborted = false; + + p->curl = curl_easy_init(); + if (!p->curl) { + MP_ERR(s, "curl_easy_init failed\n"); + return STREAM_ERROR; + } + + setup_curl(p); + mp_cancel_set_cb(s->cancel, on_cancel, p); + + cmd_sync(p, CMD_ADD, 0, false); + + mp_mutex_lock(&p->mtx); + while (!p->probed && !atomic_load_explicit(&p->aborted, memory_order_relaxed)) + mp_cond_wait(&p->cond, &p->mtx); + mp_mutex_unlock(&p->mtx); + + if (!p->stream_ok || atomic_load(&p->aborted)) + return STREAM_ERROR; + + char *content_type = NULL; + curl_easy_getinfo(p->curl, CURLINFO_CONTENT_TYPE, &content_type); + if (content_type && content_type[0]) + s->mime_type = talloc_strdup(s, content_type); + + s->seekable = p->seekable; + s->is_network = true; + s->streaming = true; + s->fast_skip = true; + s->fill_buffer = curl_fill_buffer; + s->seek = p->seekable ? curl_seek : NULL; + s->get_size = curl_get_size; + s->close = curl_close; + + return STREAM_OK; +} + +const stream_info_t stream_info_curl = { + .name = "curl", + .open2 = curl_open, + .protocols = (const char *const[]){"http", "https", NULL}, + .stream_origin = STREAM_ORIGIN_NET, +}; diff --git a/stream/stream_curl.h b/stream/stream_curl.h new file mode 100644 index 0000000000000..f0a40c002da3e --- /dev/null +++ b/stream/stream_curl.h @@ -0,0 +1,31 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#pragma once + +#include + +#include "config.h" + +struct mpv_global; + +#if HAVE_LIBCURL +// Initialize libcurl state, must be called before stream_curl is used. +void mp_curl_global_init(struct mpv_global *global); +#else +static inline void mp_curl_global_init(struct mpv_global *global) {} +#endif From f85c5a26820c312f460773bcf0e8629dfa21a9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 9 May 2026 10:07:36 +0200 Subject: [PATCH 09/17] stream_curl: allow reading cookies not only from a local file --- stream/stream_curl.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/stream/stream_curl.c b/stream/stream_curl.c index 6fd855ed37cf2..2a146f0c022d8 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -649,12 +649,23 @@ static void setup_curl(struct priv *p) } if (p->net_opts->cookies_enabled) { + curl_easy_setopt(c, CURLOPT_COOKIEFILE, ""); char *file = p->net_opts->cookies_file; if (file && file[0]) { - char *path = mp_get_user_path(p, p->global, file); - curl_easy_setopt(c, CURLOPT_COOKIEFILE, path); - } else { - curl_easy_setopt(c, CURLOPT_COOKIEFILE, ""); + void *tmp = talloc_new(NULL); + char *path = mp_get_user_path(tmp, p->global, file); + bstr data = stream_read_file2(path, tmp, + STREAM_READ_FILE_FLAGS_DEFAULT & ~STREAM_LOCAL_FS_ONLY, + p->global, 1000000); + bstr buf = data; + while (buf.len) { + bstr line = bstr_strip_linebreaks(bstr_getline(buf, &buf)); + if (!line.len) + continue; + char *line_str = bstrto0(tmp, line); + curl_easy_setopt(c, CURLOPT_COOKIELIST, line_str); + } + talloc_free(tmp); } } From 3343960f5c4432a386279c7fce2a7db3f1bc501d 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 10/17] 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 | 37 +++++++++- stream/stream_curl.c | 162 +++++++++++++++++++++++++++++++++++++++++++ stream/stream_curl.h | 51 +++++++++----- 3 files changed, 232 insertions(+), 18 deletions(-) diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index 31e88bc15d19f..470656521a966 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,21 @@ 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 = mp_curl_avio_open(demuxer, pb, &curl_data, url, flags, options, + s->protocol_whitelist, s->protocol_blacklist); + mp_assert(r != 0 || curl_data != NULL); + 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 +975,20 @@ static int nested_io_close2(struct AVFormatContext *s, AVIOContext *pb) mp_require(demuxer); lavf_priv_t *priv = demuxer->priv; + 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 (curl_data) { + mp_curl_avio_close(pb, curl_data); + return 0; + } + return priv->default_io_close2(s, pb); } @@ -1350,6 +1369,20 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check) avfc->io_open = block_io_open; } + // 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 && mp_curl_is_available(demuxer->global)) { + 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); + } + mp_set_avdict(&dopts, lavfdopts->avopts); if (av_dict_copy(&priv->av_opts, dopts, 0) < 0) { @@ -1506,6 +1539,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 2a146f0c022d8..e9fbfdb1485f2 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" @@ -298,6 +305,11 @@ void mp_curl_global_init(struct mpv_global *global) mp_require(!mp_thread_create(&ctx->thread, curl_thread, ctx)); } +bool mp_curl_is_available(struct mpv_global *global) +{ + return global && global->curl; +} + // Curl callbacks static bool is_http_success(long resp) @@ -822,3 +834,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); + + if (!mp_curl_is_available(demuxer->global)) + return AVERROR(ENOSYS); + + // 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 f0a40c002da3e..7439ebe31238d 100644 --- a/stream/stream_curl.h +++ b/stream/stream_curl.h @@ -1,31 +1,48 @@ -/* - * This file is part of mpv. - * - * mpv is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * mpv is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with mpv. If not, see . - */ - #pragma once #include #include "config.h" +#include + struct mpv_global; +struct demuxer; #if HAVE_LIBCURL // Initialize libcurl state, must be called before stream_curl is used. void mp_curl_global_init(struct mpv_global *global); + +// Returns true if the libcurl backend is built in and a global curl context +// has been initialized. +bool mp_curl_is_available(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); #else static inline void mp_curl_global_init(struct mpv_global *global) {} +static inline bool mp_curl_is_available(struct mpv_global *global) { return false; } +static inline 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) +{ + return AVERROR(ENOSYS); +} +static inline void mp_curl_avio_close(AVIOContext *pb, void *data) {} #endif From e7423b66092feef92ad0db66230112a60369d6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 8 May 2026 02:16:33 +0200 Subject: [PATCH 11/17] stream_curl: build protocol list based on libcurl state Allow only subset of supported protocols by libcurl. --- stream/stream_curl.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/stream/stream_curl.c b/stream/stream_curl.c index e9fbfdb1485f2..d31a004a3fe7b 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -828,10 +828,31 @@ static int curl_open(stream_t *s, const struct stream_open_args *args) return STREAM_OK; } +static const char *const enabled_protocols[] = {"http", "https", NULL}; + +static bool curl_has_proto(bstr proto) +{ + curl_version_info_data *info = curl_version_info(CURLVERSION_NOW); + mp_require(info && info->protocols); + return bstr_in_list0(proto, (char **)info->protocols); +} + +static char **curl_get_protocols(void) +{ + int num = 0; + char **protocols = NULL; + for (int i = 0; enabled_protocols[i]; i++) { + if (curl_has_proto(bstr0(enabled_protocols[i]))) + MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, enabled_protocols[i])); + } + MP_TARRAY_APPEND(NULL, protocols, num, NULL); + return protocols; +} + const stream_info_t stream_info_curl = { .name = "curl", .open2 = curl_open, - .protocols = (const char *const[]){"http", "https", NULL}, + .get_protocols = curl_get_protocols, .stream_origin = STREAM_ORIGIN_NET, }; @@ -902,7 +923,7 @@ int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out, // 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)) + if (!bstr_in_list0(scheme, (char **)enabled_protocols) || !curl_has_proto(scheme)) return AVERROR(ENOSYS); if (!mp_curl_is_available(demuxer->global)) From d38f55e5ef13b193791188bb4aa567c2f2c8d626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 7 May 2026 00:49:42 +0200 Subject: [PATCH 12/17] ytdl_hook.lua: use the new curl-max-request-size option if needed This is for stream_curl. --- player/lua/ytdl_hook.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/player/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua index 7f5fbcfd481e4..c8d3d71ab1d43 100644 --- a/player/lua/ytdl_hook.lua +++ b/player/lua/ytdl_hook.lua @@ -859,6 +859,7 @@ local function add_single_video(json) end if chunk_size < math.huge then stream_opts = append_libav_opt(stream_opts, "request_size", tostring(chunk_size)) + mp.set_property_native("file-local-options/curl-max-request-size", chunk_size) end mp.set_property_native("file-local-options/stream-lavf-o", stream_opts) From ab533a0db87d74f7beca874e2378c2050ab24d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 7 May 2026 04:22:34 +0200 Subject: [PATCH 13/17] stream: don't drop data on stream_read_more At the initial file open, the buffer may be populated with more data, than later requested by decoder, but don't drop this data. This fixes initial open to perform multiple reads of the area, especially for images that are likelly fully read for probbing already. --- stream/stream.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/stream/stream.c b/stream/stream.c index 4a5338eb2ea7f..53fb3aafff89f 100644 --- a/stream/stream.c +++ b/stream/stream.c @@ -541,7 +541,12 @@ static bool stream_read_more(struct stream *s, int forward) // Keep guaranteed seek-back. int buf_old = MPMIN(s->buf_cur - s->buf_start, s->requested_buffer_size / 2); - if (!stream_resize_buffer(s, buf_old + forward_avail, buf_old + forward)) + // Never shrink the buffer here. That's stream_drop_buffers()'s job. Otherwise + // data fetched by earlier larger reads (e.g. demuxer probing) would be + // discarded, forcing redundant re-reads on backward seeks. + int new_size = MPMAX(buf_old + forward, s->buffer_mask + 1); + + if (!stream_resize_buffer(s, buf_old + forward_avail, new_size)) return false; int buf_alloc = s->buffer_mask + 1; From 393092b1351994745154afd1af0a2db168098a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 8 May 2026 10:00:25 +0200 Subject: [PATCH 14/17] stream_curl: use for ftp/ftps It's significantly more stable than Lavf. Fixes seeking out of cached ranges. Additionally correctly marks demuxer_is_network for ftp too. --- DOCS/interface-changes/http-backend.txt | 2 +- DOCS/man/options.rst | 16 +-- stream/stream_curl.c | 147 ++++++++++++++++++------ 3 files changed, 123 insertions(+), 42 deletions(-) diff --git a/DOCS/interface-changes/http-backend.txt b/DOCS/interface-changes/http-backend.txt index 291cccaae3cee..1827cb9b8a2e3 100644 --- a/DOCS/interface-changes/http-backend.txt +++ b/DOCS/interface-changes/http-backend.txt @@ -1 +1 @@ -add libcurl-based stream backend, with new options `--curl-http-version`, `--curl-max-redirects`, `--curl-max-retries`, `--curl-connect-timeout`, `--curl-buffer-size` and `--curl-max-request-size` +add libcurl-based stream backend (http, https, ftp, ftps), with new options `--curl-http-version`, `--curl-max-redirects`, `--curl-max-retries`, `--curl-connect-timeout`, `--curl-buffer-size` and `--curl-max-request-size` diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index a13fda9ce4ffc..5b7ec8f38af51 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -5718,13 +5718,13 @@ Network The bitrate as used is sent by the server, and there's no guarantee it's actually meaningful. -HTTP backend (libcurl) ----------------------- +Network backend (libcurl) +------------------------- -When mpv is built with libcurl support, ``http://`` and ``https://`` URLs are -served by an internal libcurl-based stream backend instead of FFmpeg. The -backend fully supports all features of libcurl, making it more robust and -compatible with a wide range of servers and CDNs, and faster too. +When mpv is built with libcurl support, ``http://``, ``https://``, ``ftp://`` +and ``ftps://`` URLs are served by an internal libcurl-based stream backend +instead of FFmpeg. The backend fully supports all features of libcurl, making it +more robust and compatible with a wide range of servers and CDNs, and faster too. For HTTP transfers, the backend transparently negotiates HTTP/1.1, HTTP/2 multiplexing or HTTP/3 (QUIC) when the server offers them, with HSTS enabled @@ -5738,8 +5738,8 @@ The backend honors the network options listed above (``--user-agent``, ``--http-proxy``, ``--http-header-fields``, ``--referrer``, ``--cookies*``, ``--tls-*``). -If libcurl is not available at build time, or if this backend fails to handle -a particular request, mpv falls back to FFmpeg's HTTP implementation. +If libcurl is not available at build time, mpv uses FFmpeg's networking +implementation instead. To inspect libcurl's debug output (requests, response headers, TLS/connection diagnostics), set ``--msg-level=curl=trace``. diff --git a/stream/stream_curl.c b/stream/stream_curl.c index d31a004a3fe7b..8742db18b2257 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -47,6 +47,23 @@ #include "osdep/threads.h" #include "osdep/timer.h" +enum curl_proto { + MP_CURL_PROTO_HTTP, + MP_CURL_PROTO_FTP, +}; + +struct curl_scheme { + bstr scheme; + enum curl_proto proto; +}; + +static const struct curl_scheme curl_schemes[] = { + {bstr0_lit("http"), MP_CURL_PROTO_HTTP}, + {bstr0_lit("https"), MP_CURL_PROTO_HTTP}, + {bstr0_lit("ftp"), MP_CURL_PROTO_FTP}, + {bstr0_lit("ftps"), MP_CURL_PROTO_FTP}, +}; + struct curl_opts { int http_version; int max_redirects; @@ -97,6 +114,16 @@ const struct m_sub_options curl_conf = { .size = sizeof(struct curl_opts), }; +static const struct curl_scheme *curl_scheme_lookup(bstr url) +{ + bstr scheme = mp_split_proto(url, NULL); + for (int i = 0; i < MP_ARRAY_SIZE(curl_schemes); i++) { + if (bstrcasecmp(scheme, curl_schemes[i].scheme) == 0) + return &curl_schemes[i]; + } + return NULL; +} + struct curl_ctx { mp_thread thread; struct mp_dispatch_queue *dispatch; @@ -117,6 +144,7 @@ struct priv { CURL *curl; struct curl_slist *headers; char *url; + const struct curl_scheme *scheme; // Stream parameters bool seekable; @@ -317,6 +345,7 @@ static bool is_http_success(long resp) return resp >= 200 && resp < 300; } +// Called per chunk of body data. static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { struct priv *p = userdata; @@ -382,25 +411,35 @@ static const char *header_value(CURL *c, const char *name) return NULL; } -// Decide stream parameters from the header response. The stream is considered -// non-seakabke if compression is used, or server doesn't support byte ranges. -static size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) +static void finalize_probe(struct priv *p) { - struct priv *p = userdata; - size_t bytes = size * nitems; + if (mp_msg_test(p->log, MSGL_DEBUG)) { + long resp = 0; + char *ctype = NULL; + curl_easy_getinfo(p->curl, CURLINFO_RESPONSE_CODE, &resp); + curl_easy_getinfo(p->curl, CURLINFO_CONTENT_TYPE, &ctype); + MP_DBG(p, "proto=%.*s ok=%d code=%ld size=%" PRId64 " seekable=%d type=%s\n", + BSTR_P(p->scheme->scheme), p->stream_ok, resp, + p->content_size, p->seekable, ctype ? ctype : "-"); + } - if (p->probed) - return bytes; + mp_mutex_lock(&p->mtx); + p->probed = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); +} - bstr line = bstr_strip_linebreaks((bstr){buffer, bytes}); +// Empty line is the end of the header. Skip intermediate 1xx and 3xx responses, +// we care about the final one. +static void probe_http(struct priv *p, struct bstr line) +{ if (line.len > 0) - return bytes; + return; long resp = 0; curl_easy_getinfo(p->curl, CURLINFO_RESPONSE_CODE, &resp); - // Skip 1xx and intermediate 3xx (curl follows redirects internally). if (resp < 200 || (resp >= 300 && resp < 400)) - return bytes; + return; if (!is_http_success(resp)) { MP_ERR(p, "HTTP error %ld\n", resp); @@ -431,15 +470,52 @@ static size_t header_callback(char *buffer, size_t size, size_t nitems, void *us p->content_size = total; } p->stream_ok = true; +done: + finalize_probe(p); +} + +static void probe_ftp(struct priv *p, struct bstr line) +{ + if (line.len < 4 || line.start[3] != ' ') + return; + // Parse the line directly: libcurl only stamps CURLINFO_RESPONSE_CODE after + // a reply is fully processed, so polling it from header_callback returns + // the previous code. + struct bstr code = {line.start, 3}; + if (!bstr_equals0(code, "150") && !bstr_equals0(code, "125")) + return; + + curl_off_t cl = -1; + if (curl_easy_getinfo(p->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, + &cl) == CURLE_OK && cl >= 0) + p->content_size = cl; + + p->seekable = p->content_size > 0; + p->stream_ok = true; + finalize_probe(p); +} - MP_DBG(p, "status=%ld compressed=%d size=%" PRId64 " seekable=%d\n", - resp, compressed, p->content_size, p->seekable); +// Called per header line. +static size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) +{ + struct priv *p = userdata; + size_t bytes = size * nitems; + + if (p->probed) + return bytes; + + struct bstr line = bstr_strip_linebreaks((bstr){buffer, bytes}); + switch (p->scheme->proto) { + case MP_CURL_PROTO_HTTP: + probe_http(p, line); + break; + case MP_CURL_PROTO_FTP: + probe_ftp(p, line); + break; + default: + break; + } -done: - mp_mutex_lock(&p->mtx); - p->probed = true; - mp_cond_broadcast(&p->cond); - mp_mutex_unlock(&p->mtx); return bytes; } @@ -784,6 +860,9 @@ static int curl_open(stream_t *s, const struct stream_open_args *args) p->opts = mp_get_config_group(p, s->global, &curl_conf); p->net_opts = mp_get_config_group(p, s->global, &mp_network_conf); p->url = talloc_strdup(p, s->url); + p->scheme = curl_scheme_lookup(bstr0(p->url)); + // Only supported URLs are supposed to reach here. + mp_assert(p->scheme); p->content_size = -1; p->buffer_size = p->opts->buffer_size; p->buffer = talloc_size(p, p->buffer_size); @@ -828,22 +907,21 @@ static int curl_open(stream_t *s, const struct stream_open_args *args) return STREAM_OK; } -static const char *const enabled_protocols[] = {"http", "https", NULL}; - -static bool curl_has_proto(bstr proto) +static bool curl_has_proto(bstr scheme) { curl_version_info_data *info = curl_version_info(CURLVERSION_NOW); mp_require(info && info->protocols); - return bstr_in_list0(proto, (char **)info->protocols); + return bstr_in_list0(scheme, (char **)info->protocols); } static char **curl_get_protocols(void) { int num = 0; char **protocols = NULL; - for (int i = 0; enabled_protocols[i]; i++) { - if (curl_has_proto(bstr0(enabled_protocols[i]))) - MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, enabled_protocols[i])); + for (int i = 0; i < MP_ARRAY_SIZE(curl_schemes); i++) { + bstr scheme = curl_schemes[i].scheme; + if (curl_has_proto(scheme)) + MP_TARRAY_APPEND(NULL, protocols, num, bstrdup0(protocols, scheme)); } MP_TARRAY_APPEND(NULL, protocols, num, NULL); return protocols; @@ -865,15 +943,18 @@ struct curl_avio_cookie { struct mp_cancel *cancel; }; -static bool is_protocol_allowed(struct mp_log *log, const char *scheme, +static bool is_protocol_allowed(struct mp_log *log, bstr 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); + // `scheme` is required to be wrapped null-terminated string literal. + // This is UB otherwise, see curl_schemes. + mp_assert(scheme.len && scheme.start[scheme.len] == '\0'); + if (whitelist && av_match_list(scheme.start, whitelist, ',') <= 0) { + mp_err(log, "Protocol '%.*s' not on whitelist '%s'!\n", BSTR_P(scheme), whitelist); return false; } - if (blacklist && av_match_list(scheme, blacklist, ',') > 0) { - mp_err(log, "Protocol '%s' on blacklist '%s'!\n", scheme, blacklist); + if (blacklist && av_match_list(scheme.start, blacklist, ',') > 0) { + mp_err(log, "Protocol '%.*s' on blacklist '%s'!\n", BSTR_P(scheme), blacklist); return false; } return true; @@ -922,8 +1003,8 @@ int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out, 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 **)enabled_protocols) || !curl_has_proto(scheme)) + const struct curl_scheme *cs = curl_scheme_lookup(bstr0(url)); + if (!cs || !curl_has_proto(cs->scheme)) return AVERROR(ENOSYS); if (!mp_curl_is_available(demuxer->global)) @@ -939,7 +1020,7 @@ int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out, blacklist = e->value; } - if (!is_protocol_allowed(demuxer->log, bstrdup0(demuxer, scheme), whitelist, blacklist)) + if (!is_protocol_allowed(demuxer->log, cs->scheme, whitelist, blacklist)) return AVERROR(EINVAL); // Each nested stream gets its own mp_cancel slaved to the main demuxer, From 6ad95769509de0f7fa250d662371f9c569a21fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:36:16 +0200 Subject: [PATCH 15/17] stream/network: add Icecast metadata parser --- stream/network.c | 168 +++++++++++++++++++++++++++++++++++++++++++++++ stream/network.h | 40 +++++++++++ 2 files changed, 208 insertions(+) diff --git a/stream/network.c b/stream/network.c index b2c7ce90f0fe7..a339e2f834fe4 100644 --- a/stream/network.c +++ b/stream/network.c @@ -16,9 +16,17 @@ */ #include +#include +#include "common/common.h" +#include "common/tags.h" +#include "demux/demux.h" +#include "misc/charset_conv.h" +#include "mpv_talloc.h" #include "network.h" +#include "options/m_config.h" #include "options/m_option.h" +#include "stream.h" #define OPT_BASE_STRUCT struct mp_network_opts @@ -43,3 +51,163 @@ const struct m_sub_options mp_network_conf = { .timeout = 60, }, }; + +struct mp_icy { + uint64_t metaint; // bytes between metadata blocks (0 = no ICY) + uint64_t data_read; // data bytes since last metadata block + enum { + ICY_DATA = 0, + ICY_LEN, + ICY_META, + } state; + size_t meta_pending; // bytes left in the current metadata block + size_t meta_pos; // bytes already accumulated in meta_buf + char meta_buf[255 * 16 + 1]; + bstr headers; // accumulated "Icy-Name: value\n" lines + bstr packet; // last metadata payload + bool dirty; // new metadata to deliver +}; + +struct mp_icy *mp_icy_new(void *ta_parent) +{ + return talloc_zero(ta_parent, struct mp_icy); +} + +void mp_icy_reset(struct mp_icy *i) +{ + i->metaint = 0; + i->data_read = 0; + i->state = ICY_DATA; + i->meta_pending = 0; + i->meta_pos = 0; + i->headers.len = 0; + i->packet.len = 0; + i->dirty = false; +} + +void mp_icy_add_header(struct mp_icy *i, bstr line) +{ + bstr name, val; + if (!bstr_split_tok(line, ": ", &name, &val)) + return; + if (!bstr_case_startswith(name, bstr0("Icy-"))) + return; + + if (bstrcasecmp0(name, "Icy-MetaInt") == 0) { + long long mi = bstrtoll(val, NULL, 10); + if (mi > 0) + i->metaint = mi; + } + // This may look a bit weird, that we join headers again, but it's done + // to share common parse function with lavf format later. + bstr_xappend_asprintf(i, &i->headers, "%.*s: %.*s\n", BSTR_P(name), BSTR_P(val)); + printf("ICY header: %.*s: %.*s\n", BSTR_P(name), BSTR_P(val)); + i->dirty = true; +} + +bool mp_icy_active(const struct mp_icy *i) +{ + return i->metaint > 0; +} + +void mp_icy_process(struct mp_icy *i, const char *buf, size_t len, + mp_icy_write_fn write_cb, void *ctx) +{ + if (!mp_icy_active(i)) { + if (len) + write_cb(ctx, buf, len); + return; + } + size_t pos = 0; + while (pos < len) { + switch (i->state) { + case ICY_DATA: { + size_t budget = i->metaint - i->data_read; + size_t take = MPMIN(len - pos, budget); + write_cb(ctx, buf + pos, take); + pos += take; + i->data_read += take; + if (i->data_read == i->metaint) + i->state = ICY_LEN; + break; + } + case ICY_LEN: { + uint8_t n = (uint8_t)buf[pos++]; + if (n == 0) { + i->state = ICY_DATA; + i->data_read = 0; + } else { + i->meta_pending = (size_t)n * 16; + i->meta_pos = 0; + i->state = ICY_META; + } + break; + } + case ICY_META: { + size_t take = MPMIN(len - pos, i->meta_pending); + memcpy(i->meta_buf + i->meta_pos, buf + pos, take); + i->meta_pos += take; + i->meta_pending -= take; + pos += take; + if (i->meta_pending == 0) { + i->meta_buf[i->meta_pos] = '\0'; + i->packet.len = 0; + bstr_xappend_asprintf(i, &i->packet, "%s\n", i->meta_buf); + i->dirty = true; + i->state = ICY_DATA; + i->data_read = 0; + } + break; + } + } + } +} + +struct mp_tags *mp_icy_get_metadata(struct mp_icy *i, struct stream *s) +{ + if (!i->dirty) + return NULL; + i->dirty = false; + return mp_parse_icy_metadata(s, i->headers, i->packet); +} + +struct mp_tags *mp_parse_icy_metadata(struct stream *s, bstr headers, bstr packet) +{ + if (!headers.len && !packet.len) + return NULL; + + struct mp_tags *res = talloc_zero(NULL, struct mp_tags); + + while (headers.len) { + bstr line = bstr_strip_linebreaks(bstr_getline(headers, &headers)); + bstr name, val; + if (bstr_split_tok(line, ": ", &name, &val)) + mp_tags_set_bstr(res, name, val); + } + + bstr head = bstr0("StreamTitle='"); + int i = bstr_find(packet, head); + if (i >= 0) { + packet = bstr_cut(packet, i + head.len); + int end = bstr_find(packet, bstr0("\';")); + if (end >= 0) + packet = bstr_splice(packet, 0, end); + + bool allocated = false; + struct demux_opts *opts = mp_get_config_group(NULL, s->global, &demux_conf); + const char *charset = mp_charset_guess(s, s->log, packet, opts->meta_cp, 0); + if (charset && !mp_charset_is_utf8(charset)) { + bstr conv = mp_iconv_to_utf8(s->log, packet, charset, 0); + if (conv.start && conv.start != packet.start) { + allocated = true; + packet = conv; + } + } + mp_tags_set_bstr(res, bstr0("icy-title"), packet); + talloc_free(opts); + if (allocated) + talloc_free(packet.start); + } + + return res; +} diff --git a/stream/network.h b/stream/network.h index d7516b2e43438..dd0498652c118 100644 --- a/stream/network.h +++ b/stream/network.h @@ -20,6 +20,9 @@ #include struct m_sub_options; +struct mp_tags; +struct stream; +typedef struct bstr bstr; struct mp_network_opts { bool cookies_enabled; @@ -36,3 +39,40 @@ struct mp_network_opts { }; extern const struct m_sub_options mp_network_conf; + +// Build mp_tags from accumulated ICY metadata. `headers` is a buffer of +// "Icy-*: value\n" lines collected from the response. `packet` is the most +// recent in-band metadata payload. Returns NULL when both are empty. +// Returned value has to be freed by the caller. +struct mp_tags *mp_parse_icy_metadata(struct stream *s, bstr headers, + bstr packet); + +// Opaque state for receiving ICY (Shoutcast/Icecast) metadata over an HTTP +// stream. This wrapper is not thread safe. +struct mp_icy; + +// Allocate a new ICY context as a talloc child of `ta_parent`. +struct mp_icy *mp_icy_new(void *ta_parent); + +// Reset all ICY state. Use between responses (e.g. across redirects). +void mp_icy_reset(struct mp_icy *icy); + +// Feed a single response header line (without trailing CRLF). Lines that +// don't start with "Icy-" are silently ignored. +void mp_icy_add_header(struct mp_icy *icy, bstr line); + +// True if Icy-MetaInt was seen and the body must be filtered. +bool mp_icy_active(const struct mp_icy *icy); + +// Body callback used by mp_icy_process(). Invoked once per contiguous +// stretch of non-metadata bytes. +typedef void (*mp_icy_write_fn)(void *ctx, const char *data, size_t len); + +// Process a body chunk. When ICY is active, metadata bytes are stripped and +// stashed internally, remaining data is delivered via `write_cb`. Otherwise +// the chunk is forwarded as a single call to `write_cb`. +void mp_icy_process(struct mp_icy *icy, const char *buf, size_t len, + mp_icy_write_fn write_cb, void *ctx); + +// Returns talloc-allocated mp_tags. +struct mp_tags *mp_icy_get_metadata(struct mp_icy *icy, struct stream *s); From ff43fafbb287f6d5e8ffad533e507925a28cf5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:37:44 +0200 Subject: [PATCH 16/17] stream_lavf: use common icy metadata parser --- stream/stream_lavf.c | 48 ++++---------------------------------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/stream/stream_lavf.c b/stream/stream_lavf.c index 868f227cc73a0..7457a1b85fe77 100644 --- a/stream/stream_lavf.c +++ b/stream/stream_lavf.c @@ -22,10 +22,7 @@ #include "options/path.h" #include "common/common.h" #include "common/msg.h" -#include "common/tags.h" #include "common/av_common.h" -#include "demux/demux.h" -#include "misc/charset_conv.h" #include "misc/thread_tools.h" #include "stream.h" #include "network.h" @@ -456,51 +453,14 @@ static struct mp_tags *read_icy(stream_t *s) // Send a metadata update only 1. on start, and 2. on a new metadata packet. // To detect new packages, set the icy_metadata_packet to "-" once we've // read it (a bit hacky, but works). - struct mp_tags *res = NULL; - if ((!icy_header || !icy_header[0]) && (!icy_packet || !icy_packet[0])) - goto done; - bstr packet = bstr0(icy_packet); - if (bstr_equals0(packet, "-")) - goto done; - - res = talloc_zero(NULL, struct mp_tags); - - bstr header = bstr0(icy_header); - while (header.len) { - bstr line = bstr_strip_linebreaks(bstr_getline(header, &header)); - bstr name, val; - if (bstr_split_tok(line, ": ", &name, &val)) - mp_tags_set_bstr(res, name, val); - } - - bstr head = bstr0("StreamTitle='"); - int i = bstr_find(packet, head); - if (i >= 0) { - packet = bstr_cut(packet, i + head.len); - int end = bstr_find(packet, bstr0("\';")); - packet = bstr_splice(packet, 0, end); - - bool allocated = false; - struct demux_opts *opts = mp_get_config_group(NULL, s->global, &demux_conf); - const char *charset = mp_charset_guess(s, s->log, packet, opts->meta_cp, 0); - if (charset && !mp_charset_is_utf8(charset)) { - bstr conv = mp_iconv_to_utf8(s->log, packet, charset, 0); - if (conv.start && conv.start != packet.start) { - allocated = true; - packet = conv; - } - } - mp_tags_set_bstr(res, bstr0("icy-title"), packet); - talloc_free(opts); - if (allocated) - talloc_free(packet.start); - } + if (!bstr_equals0(packet, "-")) + res = mp_parse_icy_metadata(s, bstr0(icy_header), packet); - av_opt_set(avio, "icy_metadata_packet", "-", AV_OPT_SEARCH_CHILDREN); + if (res) + av_opt_set(avio, "icy_metadata_packet", "-", AV_OPT_SEARCH_CHILDREN); -done: av_free(icy_header); av_free(icy_packet); return res; From f510cb0de9c24ab20c7d7ed898171b137d7f8abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 11 May 2026 22:39:00 +0200 Subject: [PATCH 17/17] stream_curl: add support for Icecast metadata --- stream/stream_curl.c | 60 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/stream/stream_curl.c b/stream/stream_curl.c index 8742db18b2257..fde26642248a2 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -171,6 +171,7 @@ struct priv { bool stream_eof; // producer has delivered all data bool stream_error; // unrecoverable error atomic_bool aborted; // canceled by user (mp_cancel) + struct mp_icy *icy; // ICY metadata state, dormant until Icy-MetaInt seen }; // Curl thread @@ -345,6 +346,18 @@ static bool is_http_success(long resp) return resp >= 200 && resp < 300; } +// Append `len` bytes to the ring buffer. Caller must hold p->mtx and have +// verified that there is enough free space. +static void ring_write(void *ctx, const char *data, size_t len) +{ + struct priv *p = ctx; + size_t tail_chunk = MPMIN(p->buffer_size - p->tail, len); + memcpy(p->buffer + p->tail, data, tail_chunk); + memcpy(p->buffer, data + tail_chunk, len - tail_chunk); + p->tail = (p->tail + len) % p->buffer_size; + p->count += len; +} + // Called per chunk of body data. static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { @@ -369,13 +382,9 @@ static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdat return CURL_WRITEFUNC_PAUSE; } - size_t tail_chunk = MPMIN(p->buffer_size - p->tail, bytes); - memcpy(p->buffer + p->tail, ptr, tail_chunk); - memcpy(p->buffer, ptr + tail_chunk, bytes - tail_chunk); - p->tail = (p->tail + bytes) % p->buffer_size; - p->count += bytes; - p->paused = false; + mp_icy_process(p->icy, ptr, bytes, ring_write, p); + p->paused = false; p->request_received += bytes; mp_cond_broadcast(&p->cond); @@ -433,8 +442,18 @@ static void finalize_probe(struct priv *p) // we care about the final one. static void probe_http(struct priv *p, struct bstr line) { - if (line.len > 0) + if (line.len > 0) { + // A new status line resets per-response state so that intermediate + // 1xx/3xx responses don't leak ICY metadata into the final one. + mp_mutex_lock(&p->mtx); + if (bstr_startswith0(line, "HTTP/")) { + mp_icy_reset(p->icy); + } else { + mp_icy_add_header(p->icy, line); + } + mp_mutex_unlock(&p->mtx); return; + } long resp = 0; curl_easy_getinfo(p->curl, CURLINFO_RESPONSE_CODE, &resp); @@ -454,8 +473,10 @@ static void probe_http(struct priv *p, struct bstr line) bool accept_ranges = ar && strcasecmp(ar, "bytes") == 0; // Some servers reply 200 to an open-ended "Range: 0-" but 206 to explicit - // byte ranges, so trust either. - p->seekable = !compressed && (resp == 206 || accept_ranges); + // byte ranges, so trust either. ICY metadata is interleaved with the body, + // so byte ranges from the server don't line up with consumer offsets. + p->seekable = !compressed && !mp_icy_active(p->icy) && + (resp == 206 || accept_ranges); if (p->seekable) { // Content-Range carries the full size on a partial response. On any @@ -682,6 +703,8 @@ static struct curl_slist *build_header_list(struct priv *p) for (int i = 0; p->net_opts->http_header_fields[i]; i++) list = curl_slist_append(list, p->net_opts->http_header_fields[i]); } + if (p->scheme->proto == MP_CURL_PROTO_HTTP) + list = curl_slist_append(list, "Icy-MetaData: 1"); return list; } @@ -813,6 +836,23 @@ static int64_t curl_get_size(struct stream *s) return p->content_size; } +static int curl_control(struct stream *s, int cmd, void *arg) +{ + struct priv *p = s->priv; + switch (cmd) { + case STREAM_CTRL_GET_METADATA: { + mp_mutex_lock(&p->mtx); + struct mp_tags *tags = mp_icy_get_metadata(p->icy, s); + mp_mutex_unlock(&p->mtx); + if (!tags) + break; + *(struct mp_tags **)arg = tags; + return STREAM_OK; + } + } + return STREAM_UNSUPPORTED; +} + static void priv_destructor(void *ptr) { struct priv *p = ptr; @@ -866,6 +906,7 @@ static int curl_open(stream_t *s, const struct stream_open_args *args) p->content_size = -1; p->buffer_size = p->opts->buffer_size; p->buffer = talloc_size(p, p->buffer_size); + p->icy = mp_icy_new(p); mp_mutex_init(&p->mtx); mp_cond_init(&p->cond); @@ -902,6 +943,7 @@ static int curl_open(stream_t *s, const struct stream_open_args *args) s->fill_buffer = curl_fill_buffer; s->seek = p->seekable ? curl_seek : NULL; s->get_size = curl_get_size; + s->control = curl_control; s->close = curl_close; return STREAM_OK;