diff --git a/DOCS/interface-changes/http-backend.txt b/DOCS/interface-changes/http-backend.txt new file mode 100644 index 0000000000..291cccaae3 --- /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 cb9e74c6da..e93cafe06f 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 c05b70ec1a..6cdbea552d 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 d6b74ea06a..65a9ed320e 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 836d16d03f..35e174ceba 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 a64e3e9f6f..48baef13ff 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 9275f8c259..014e41de6d 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 23e89005e3..6891c9d5a1 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,9 @@ struct MPContext *mp_create(void) demux_packet_pool_init(mpctx->global); stats_global_init(mpctx->global); +#if HAVE_LIBCURL + mp_curl_global_init(mpctx->global); +#endif // 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 03dd7ba5e6..4a5338eb2e 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 0000000000..6fd855ed37 --- /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 0000000000..10750819b6 --- /dev/null +++ b/stream/stream_curl.h @@ -0,0 +1,27 @@ +/* + * 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; + +// Initialize libcurl state, must be called before stream_curl is used. +void mp_curl_global_init(struct mpv_global *global);