stream_curl: add libcurl-based stream backend
Introduces stream_http, an internal libcurl-driven backend for http:// and https:// URLs. Runs all transfers on a dedicated curl multi thread with a producer-side ring buffer, and exposes HTTP/2 multiplexing, QUIC/HTTP/3 (when libcurl supports it), HSTS and TCP keep-alive. Should generally be more stable and faster than FFmpeg HTTP/1.1 impl, additionally connections are kept-alive between files open, so if you open playlist of network files and navigate through them, it will re-use the same connection. Build is gated on the new 'libcurl' meson option (auto). When disabled or unavailable, mpv silently falls back to FFmpeg's HTTP implementation.
This commit is contained in:
1 parent
5936572e1e
commit
c6833a9307
11 files changed
+927
No files matched your search
@@ -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`
|
||||
@@ -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=<auto|1.0|1.1|2|2tls|2-prior-knowledge|3|3only>``
|
||||
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=<seconds>``
|
||||
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=<bytes>``
|
||||
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=<bytes>``
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ struct mpv_global {
|
||||
char *configdir;
|
||||
struct stats_base *stats;
|
||||
struct demux_packet_pool *packet_pool;
|
||||
struct curl_ctx *curl;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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!',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#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,
|
||||
};
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#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);
|
||||
Reference in new issue
Block a user