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 diff --git a/DOCS/interface-changes/http-backend.txt b/DOCS/interface-changes/http-backend.txt new file mode 100644 index 0000000000000..1827cb9b8a2e3 --- /dev/null +++ b/DOCS/interface-changes/http-backend.txt @@ -0,0 +1 @@ +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 ed4eb7e4e8879..5b7ec8f38af51 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. +Network backend (libcurl) +------------------------- + +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 +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, mpv uses FFmpeg's networking +implementation instead. + +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/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[@]}" .. 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 ` 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/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/meson.build b/meson.build index ab09af61177a8..65a9ed320e741 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', @@ -620,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 66a5a6f7268f9..48baef13ff731 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; @@ -94,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; @@ -672,6 +674,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 -------------------- @@ -924,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 a8950df4a2920..014e41de6d27e 100644 --- a/options/options.h +++ b/options/options.h @@ -369,7 +369,8 @@ 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 mp_network_opts *network_opts; + struct stream_lavf_opts *stream_lavf_opts; struct demux_rawaudio_opts *demux_rawaudio; struct demux_rawvideo_opts *demux_rawvideo; @@ -394,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/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua index 2f2e6309470b8..c8d3d71ab1d43 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 @@ -873,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) @@ -1061,7 +1048,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 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/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; diff --git a/stream/network.c b/stream/network.c new file mode 100644 index 0000000000000..a339e2f834fe4 --- /dev/null +++ b/stream/network.c @@ -0,0 +1,213 @@ +/* + * 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 "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 + +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, + }, +}; + +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 new file mode 100644 index 0000000000000..dd0498652c118 --- /dev/null +++ b/stream/network.h @@ -0,0 +1,78 @@ +/* + * 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_tags; +struct stream; +typedef struct bstr bstr; + +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; + +// 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); diff --git a/stream/stream.c b/stream/stream.c index 03dd7ba5e6baa..53fb3aafff89f 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, }; @@ -537,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; diff --git a/stream/stream_curl.c b/stream/stream_curl.c new file mode 100644 index 0000000000000..fde26642248a2 --- /dev/null +++ b/stream/stream_curl.c @@ -0,0 +1,1130 @@ +/* + * 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 +#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 "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" +#include "options/m_config.h" +#include "options/m_option.h" +#include "options/path.h" +#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; + 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), +}; + +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; + 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; + const struct curl_scheme *scheme; + + // 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) + struct mp_icy *icy; // ICY metadata state, dormant until Icy-MetaInt seen +}; + +// 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)); +} + +bool mp_curl_is_available(struct mpv_global *global) +{ + return global && global->curl; +} + +// Curl callbacks + +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) +{ + 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; + } + + mp_icy_process(p->icy, ptr, bytes, ring_write, p); + + 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; +} + +static void finalize_probe(struct priv *p) +{ + 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 : "-"); + } + + mp_mutex_lock(&p->mtx); + p->probed = true; + mp_cond_broadcast(&p->cond); + mp_mutex_unlock(&p->mtx); +} + +// 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) { + // 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); + if (resp < 200 || (resp >= 300 && resp < 400)) + return; + + 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. 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 + // 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; +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); +} + +// 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; + } + + 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]); + } + if (p->scheme->proto == MP_CURL_PROTO_HTTP) + list = curl_slist_append(list, "Icy-MetaData: 1"); + 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) { + curl_easy_setopt(c, CURLOPT_COOKIEFILE, ""); + char *file = p->net_opts->cookies_file; + if (file && file[0]) { + 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); + } + } + + 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 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; + 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->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); + p->icy = mp_icy_new(p); + + 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->control = curl_control; + s->close = curl_close; + + return STREAM_OK; +} + +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(scheme, (char **)info->protocols); +} + +static char **curl_get_protocols(void) +{ + int num = 0; + char **protocols = NULL; + 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; +} + +const stream_info_t stream_info_curl = { + .name = "curl", + .open2 = curl_open, + .get_protocols = curl_get_protocols, + .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, bstr scheme, + const char *whitelist, const char *blacklist) +{ + // `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.start, blacklist, ',') > 0) { + mp_err(log, "Protocol '%.*s' on blacklist '%s'!\n", BSTR_P(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. + 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)) + 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, cs->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 new file mode 100644 index 0000000000000..7439ebe31238d --- /dev/null +++ b/stream/stream_curl.h @@ -0,0 +1,48 @@ +#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 diff --git a/stream/stream_lavf.c b/stream/stream_lavf.c index 7910b820820d2..7457a1b85fe77 100644 --- a/stream/stream_lavf.c +++ b/stream/stream_lavf.c @@ -22,12 +22,10 @@ #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" #include "options/m_config.h" #include "options/m_option.h" @@ -36,43 +34,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 +157,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 +212,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); } @@ -478,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;