/* * libcurl based HTTP(S) protocol * Copyright (C) 2026 Kacper Michajłow * * This file is part of FFmpeg. * * FFmpeg 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. * * FFmpeg 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 FFmpeg; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "config_components.h" #include #include #include #include #include #include #include "libavutil/avstring.h" #include "libavutil/bprint.h" #include "libavutil/dict.h" #include "libavutil/error.h" #include "libavutil/fifo.h" #include "libavutil/log.h" #include "libavutil/macros.h" #include "libavutil/mem.h" #include "libavutil/opt.h" #include "libavutil/thread.h" #include "libavutil/time.h" #include "avformat.h" #include "http.h" #include "internal.h" #include "network.h" #include "url.h" #include "version.h" #define DEFAULT_USER_AGENT "Lavf/" AV_STRINGIFY(LIBAVFORMAT_VERSION) #define CURL_DEFAULT_BUFFER_SIZE (4 << 20) /* Blocking waits wake up this often so url_read()/open can poll the interrupt * callback. */ #define CURL_WAIT_US 100000 /* Cap on the accumulated "Icy-*" reply headers exported to the caller. */ #define ICY_MAX_HEADERS 65536 /* Largest in-band metadata block: the length byte counts 16 byte units. */ #define ICY_MAX_BLOCK (255 * 16) /* How many microseconds to wait before retrying a failed request; grows * exponentially (2^) with each consecutive failure up to MAX_US. Note that the * first retry is always immediate. */ #define CURL_RETRY_BASE_US 1000000 #define CURL_RETRY_MAX_US 60000000 typedef struct CurlContext CurlContext; enum cmd_kind { CMD_ADD, /* add the easy handle to the multi and start the transfer */ CMD_REMOVE, /* remove the easy handle from the multi */ CMD_UNPAUSE, /* resume a transfer paused because the FIFO was full */ CMD_SEEK, /* restart the transfer at a new byte offset */ }; typedef struct CurlCmd { enum cmd_kind kind; CurlContext *ctx; int64_t pos; /* CMD_SEEK target offset */ int sync; /* caller waits for completion */ int done; struct CurlCmd *next; } CurlCmd; typedef struct CurlLoop { AVFormatContext *avfc; /* owning context (if any) */ pthread_t thread; CURLM *multi; CURLSH *share; /* shared cookies/HSTS */ int has_cookies; /* libcurl was built with cookie support */ pthread_mutex_t mutex; /* guards the command queue, exit, share and cmd->done */ pthread_cond_t cond; /* signaled when a sync command completes */ CurlCmd *cmd_head, *cmd_tail; int exit; /* Connection statistics (updated by loop thread) */ int64_t total_bytes; int64_t total_time_us; int num_connections; int num_redirects; int num_requests; int num_errors; } CurlLoop; struct CurlContext { const AVClass *class; URLContext *h; CurlLoop *loop; int private_loop; /* loop is owned by this context (not shared) */ CURL *easy; struct curl_slist *header_list; struct curl_slist *alias_list; /* AVOptions. */ char *user_agent; char *referer; char *headers; char *http_proxy; char *cookies; char *ca_file; char *cert_file; char *key_file; char *location; /* effective URL after redirects (output) */ int64_t off; /* initial byte offset */ int64_t end_off; /* exclusive upper byte bound (0 = none) */ int tls_verify; int seekable_opt; int connect_timeout; int max_redirects; int multiple_requests; int http_version; int64_t buffer_size; int64_t request_size; int64_t initial_request_size; int64_t short_seek_size; int max_retries; int icy; int icy_status; char *icy_metadata_headers; /* "Icy-*" reply headers (output) */ char *icy_metadata_packet; /* last in-band block (output) */ AVDictionary *metadata; /* ICY metadata (output) */ /* URL thread bookkeeping, not touched by loop thread */ int64_t logical_pos; /* next byte url_read() will return, caller side */ int retry_count; /* consecutive recoverable failures */ int64_t retry_time; /* timestamp of next retry */ int64_t icy_data_read; /* payload bytes since the last block, caller side */ int icy_block_len; /* -1 while the length byte is pending */ int icy_block_filled; uint8_t icy_block[ICY_MAX_BLOCK + 1]; /* Producer bookkeeping, touched only by the loop thread. */ int active; /* currently added to the multi */ int64_t request_start; /* absolute offset the current request began at */ int64_t request_received;/* bytes delivered in the current request */ int64_t request_end; /* expected end of request, or -1 if unknown */ int is_initial; /* using reduced request size */ int seek_queued; /* soft seeking; drain remaining bytes until done */ /* Per-response-block header scratch, loop thread only. */ int hdr_accept_ranges; int hdr_compressed; int64_t hdr_content_start; /* inclusive start, or -1 */ int64_t hdr_content_end; /* inclusive end, or -1 */ int64_t hdr_content_total; /* if known, or -1 */ int64_t hdr_content_length; /* if known, or -1 */ AVDictionary *hdr_icy; /* "Icy-*" headers of this block */ int64_t hdr_icy_metaint; /* in-band metadata interval, or -1 */ /* Probe result. Set by the loop thread, read by url_open() once probed. */ int probed; int probe_ok; /* the first reply can be read */ int stream_ok; int seekable; int64_t content_size; /* Shared transfer state, guarded by mutex. */ pthread_mutex_t mutex; pthread_cond_t cond; AVFifo *fifo; int paused; /* write callback paused, FIFO was full */ int aborted; /* transfer should stop (open was interrupted) */ int status; /* current stream status (AVERROR code) */ CURLcode curl_status; /* corresponding libcurl status code */ int64_t icy_metaint; /* in-band metadata interval, 0 if none */ }; /* Guards lazy creation of a format context's shared loop. */ static AVMutex curl_loop_lock = AV_MUTEX_INITIALIZER; static int curlcode_to_averror(CURLcode code) { switch (code) { case CURLE_OK: return 0; case CURLE_URL_MALFORMAT: case CURLE_UNSUPPORTED_PROTOCOL: case CURLE_BAD_FUNCTION_ARGUMENT: return AVERROR(EINVAL); case CURLE_UNKNOWN_OPTION: case CURLE_NOT_BUILT_IN: return AVERROR(ENOSYS); case CURLE_COULDNT_RESOLVE_PROXY: case CURLE_COULDNT_RESOLVE_HOST: return AVERROR(EHOSTUNREACH); case CURLE_COULDNT_CONNECT: return AVERROR(ECONNREFUSED); case CURLE_OPERATION_TIMEDOUT: return AVERROR(ETIMEDOUT); case CURLE_LOGIN_DENIED: case CURLE_REMOTE_ACCESS_DENIED: return AVERROR(EACCES); case CURLE_OUT_OF_MEMORY: return AVERROR(ENOMEM); case CURLE_PEER_FAILED_VERIFICATION: case CURLE_SSL_CACERT_BADFILE: return AVERROR_INVALIDDATA; default: return AVERROR(EIO); } } static int curl_setopt_checked(CurlContext *c, const char *name, CURLcode code) { if (code == CURLE_OK) return 0; av_log(c->h, AV_LOG_ERROR, "Could not set %s: %s\n", name, curl_easy_strerror(code)); return curlcode_to_averror(code); } static int curlmcode_to_averror(CURLMcode code) { switch (code) { case CURLM_OK: return 0; case CURLM_UNKNOWN_OPTION: return AVERROR(EINVAL); case CURLM_OUT_OF_MEMORY: return AVERROR(ENOMEM); default: return AVERROR(EIO); } } static int curlmcode_to_curlcode(CURLMcode code) { switch (code) { case CURLM_OK: return CURLE_OK; case CURLM_UNKNOWN_OPTION: return CURLE_UNKNOWN_OPTION; case CURLM_OUT_OF_MEMORY: return CURLE_OUT_OF_MEMORY; case CURLM_ABORTED_BY_CALLBACK: return CURLE_ABORTED_BY_CALLBACK; case CURLM_UNRECOVERABLE_POLL: return CURLE_UNRECOVERABLE_POLL; default: return CURLE_FAILED_INIT; } } static int is_recoverable(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 1; default: return 0; } } /* ------------------------------------------------------------------------- */ /* curl callbacks (run on the loop thread) */ /* ------------------------------------------------------------------------- */ static void update_status_locked(CurlContext *c, int status, CURLcode code) { if (c->status) return; c->status = status; c->curl_status = code; } static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { CurlContext *c = userdata; size_t bytes = size * nmemb; size_t space; pthread_mutex_lock(&c->mutex); if (c->aborted || !c->stream_ok) { pthread_mutex_unlock(&c->mutex); return CURL_WRITEFUNC_ERROR; } if (c->seek_queued) { pthread_mutex_unlock(&c->mutex); return bytes; /* discard */ } /* Prevent overflow / non-addressable byte ranges */ if (bytes > INT64_MAX - c->request_start - c->request_received) { av_log(c->h, AV_LOG_ERROR, "Server sent back more data than addressable " "at offset %"PRId64"\n", c->request_start); c->loop->num_errors++; c->stream_ok = 0; if (!c->status) c->status = AVERROR(ERANGE); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return CURL_WRITEFUNC_ERROR; } space = av_fifo_can_write(c->fifo); if (space < bytes) { /* pause the transfer and wait for the consumer to drain. */ c->paused = 1; pthread_mutex_unlock(&c->mutex); return CURL_WRITEFUNC_PAUSE; } av_fifo_write(c->fifo, ptr, bytes); c->paused = 0; c->request_received += bytes; pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return bytes; } /* Return 1 if an offset was successfully parsed, 0 otherwise */ static int parse_offset(const char *str, int64_t *out, const char **ptr) { if (!av_isdigit(str[0])) return 0; errno = 0; char *end; int64_t val = strtoll(str, &end, 10); if (errno == ERANGE) return 0; *out = val; *ptr = end; return 1; } /* "bytes $from-$to/$document_size" */ static void parse_content_range(CurlContext *c, const char *v) { int64_t start = -1, end = -1, total = -1; while (av_isspace(*v)) v++; if (!av_stristart(v, "bytes ", &v)) return; if (!av_strstart(v, "*", &v)) { if (!parse_offset(v, &start, &v) || !av_strstart(v, "-", &v) || !parse_offset(v, &end, &v)) return; } if (!av_strstart(v, "/", &v)) return; if (!av_strstart(v, "*", &v) && !parse_offset(v, &total, &v)) return; while (av_isspace(*v)) v++; if (v[0] || (total < 0 && start < 0)) return; // reject trailing bytes or "*/*" /* only set these fields if the header was recognized; ignore otherwise */ c->hdr_content_start = start; c->hdr_content_end = end; c->hdr_content_total = total; } /* Parse a decimal header value, bounded by len since curl does not promise a * NUL terminated header buffer. Returns -1 if absent, malformed or too large. */ static int64_t parse_metaint(const char *p, size_t len) { int64_t v = 0; size_t i = 0; while (i < len && av_isspace(p[i])) i++; if (i == len || !av_isdigit(p[i])) return -1; for (; i < len && av_isdigit(p[i]); i++) { if (v > (INT_MAX - (p[i] - '0')) / 10) return -1; v = v * 10 + (p[i] - '0'); } // Reject 4junk while (i < len && av_isspace(p[i])) i++; if (i != len) return -1; return v; } /* Store one "Tag: value" reply header, tolerating a missing space after the * colon and the CRLF curl leaves on the line. */ static void store_icy_header(AVDictionary **dict, const char *ptr, size_t len) { const char *colon = memchr(ptr, ':', len); const char *val, *end = ptr + len; char *key, *value; if (!colon) return; val = colon + 1; while (val < end && av_isspace(*val)) val++; while (end > val && (end[-1] == '\r' || end[-1] == '\n')) end--; key = av_strndup(ptr, colon - ptr); value = av_strndup(val, end - val); /* Multikey keeps repeated headers in arrival order, as http.c reports them. */ if (key && value) av_dict_set(dict, key, value, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL | AV_DICT_MULTIKEY); else { av_free(key); av_free(value); } } /* Export the reply's "Icy-*" headers. Called once, before probed is set, so * that from then on only the caller thread touches the exported metadata. */ static void commit_icy_headers(CurlContext *c) { const AVDictionaryEntry *e = NULL; AVBPrint bp; if (!c->hdr_icy) return; av_bprint_init(&bp, 0, ICY_MAX_HEADERS); while ((e = av_dict_iterate(c->hdr_icy, e))) av_bprintf(&bp, "%s: %s\n", e->key, e->value); av_freep(&c->icy_metadata_headers); if (av_bprint_finalize(&bp, &c->icy_metadata_headers) < 0) c->icy_metadata_headers = NULL; av_dict_copy(&c->metadata, c->hdr_icy, 0); } static int verify_content_range(CurlContext *c, int64_t start, int64_t end, int64_t total) { if (start < 0 || start != c->request_start) { av_log(c->h, AV_LOG_ERROR, "Server sent back unexpected reply " "with offset %"PRId64" (expected %"PRId64")\n", start, c->request_start); return 0; } if (end >= 0 && end < start) { av_log(c->h, AV_LOG_ERROR, "Server sent back backwards content range " "%"PRId64"-%"PRId64"\n", start, end); return 0; } if (total >= 0 && (start >= total || end >= total)) { av_log(c->h, AV_LOG_ERROR, "Server sent back content range " "%"PRId64"-%"PRId64" that exceeds the total size " "%"PRId64"\n", start, end, total); return 0; } if (c->hdr_content_length >= 0 && end >= 0 && (c->hdr_content_length - 1 > INT64_MAX - start || start + (c->hdr_content_length - 1) != end)) { av_log(c->h, AV_LOG_ERROR, "Server sent back content range " "%"PRId64"-%"PRId64" that doesn't match the content length " "%"PRId64"\n", start, end, c->hdr_content_length); return 0; } return 1; } static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userdata) { CurlContext *c = userdata; size_t len = size * nitems; size_t n = len; long status = 0; /* Only "ICY 200" is aliased, so these two prefixes cover every status line * curl lets through. */ if (av_strncasecmp(ptr, "HTTP/", 5) == 0 || av_strncasecmp(ptr, "ICY ", 4) == 0) { c->hdr_accept_ranges = 0; c->hdr_compressed = 0; c->hdr_content_start = -1; c->hdr_content_end = -1; c->hdr_content_total = -1; c->hdr_content_length = -1; c->hdr_icy_metaint = -1; av_dict_free(&c->hdr_icy); return len; } if (av_strncasecmp(ptr, "Accept-Ranges:", 14) == 0) { c->hdr_accept_ranges = !!av_stristr(ptr + 14, "bytes"); return len; } if (av_strncasecmp(ptr, "Content-Encoding:", 17) == 0) { c->hdr_compressed = !av_stristr(ptr + 17, "identity"); return len; } if (av_strncasecmp(ptr, "Content-Range:", 14) == 0) { parse_content_range(c, ptr + 14); return len; } if (av_strncasecmp(ptr, "icy-metaint:", 12) == 0) { c->hdr_icy_metaint = parse_metaint(ptr + 12, len - 12); return len; } /* Collected per block so that headers from a redirect hop, which the * interim early return below discards, do not reach the caller. */ if (av_strncasecmp(ptr, "icy-", 4) == 0) { store_icy_header(&c->hdr_icy, ptr, len); return len; } /* Otherwise act only on the blank line that terminates the header block. */ while (n && (ptr[n - 1] == '\r' || ptr[n - 1] == '\n')) n--; if (n) return len; curl_easy_getinfo(c->easy, CURLINFO_RESPONSE_CODE, &status); /* Interim (1xx) and redirect (3xx) responses produce an intermediate header * block, wait for the final one. */ if (status < 200 || (status >= 300 && status < 400)) return len; curl_off_t cl = -1; if (curl_easy_getinfo(c->easy, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &cl) == CURLE_OK) c->hdr_content_length = cl >= 0 ? cl : -1; pthread_mutex_lock(&c->mutex); if (status >= 200 && status < 300) { int64_t content_start = status == 206 ? c->hdr_content_start : 0; int64_t content_end = status == 206 ? c->hdr_content_end : -1; int64_t content_total = status == 206 ? c->hdr_content_total : c->hdr_content_length; if (content_end < 0 && content_total > 0) content_end = content_total - 1; if (!verify_content_range(c, content_start, content_end, content_total)) { c->loop->num_errors++; c->stream_ok = 0; update_status_locked(c, AVERROR(EIO), CURLE_OK); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return len; } /* Don't unlearn a known size when a reply omits it. */ if (!c->hdr_compressed && content_total >= 0) c->content_size = content_total; if (!c->hdr_compressed) { int64_t total = content_total; if (total < 0 && status != 206) total = c->hdr_content_length; /* Don't unlearn a known size when a reply omits it. */ if (total >= 0) c->content_size = total; if (content_end < 0) content_end = c->content_size > 0 ? c->content_size - 1 : -1; } c->stream_ok = 1; c->request_end = c->hdr_compressed ? -1 : content_end; /* Capture the post-redirect URL, this is exposed as "location" AVOption * for compatibility with http.c. */ if (!c->probed) { const char *eff = NULL; if (curl_easy_getinfo(c->easy, CURLINFO_EFFECTIVE_URL, &eff) == CURLE_OK && eff) { char *dup = av_strdup(eff); if (dup) { av_free(c->location); c->location = dup; } } commit_icy_headers(c); } /* A compressed body is addressed in encoded form, so byte offsets are * meaningless: not seekable. Note that we prefer compression over * seekability, servers don't offer media in compressed form, so it * gives us free compression for other payloads like text playlist. */ c->seekable = !c->hdr_compressed && (status == 206 || c->hdr_accept_ranges); /* Apply the user override on every reply so re-evaluation of a * follow-up reply doesn't clobber it. */ if (c->seekable_opt >= 0) c->seekable = c->seekable_opt; c->icy_metaint = 0; if (c->hdr_icy_metaint > 0) { c->icy_metaint = c->hdr_icy_metaint; if (c->seekable_opt > 0) av_log(c->h, AV_LOG_WARNING, "Ignoring seekable=1, the " "reply carries in-band ICY metadata\n"); /* In-band metadata makes byte offsets meaningless. Staying * non-seekable also keeps on_done() from restarting a request * without draining the FIFO, which would desync the * interleave. */ c->seekable = 0; c->content_size = -1; } } else if (status == 416 && c->probed && c->content_size < 0 && (c->hdr_content_total < 0 || c->hdr_content_total <= c->request_start)) { /* A 416 on a follow-up request of a file of unknown size is EOF, unless * it states a size past the requested offset. */ c->content_size = c->hdr_content_total; c->stream_ok = 0; update_status_locked(c, AVERROR_EOF, CURLE_OK); } else { c->loop->num_errors++; c->stream_ok = 0; update_status_locked(c, ff_http_averror(status, AVERROR(EIO)), CURLE_OK); } if (!c->probed) { c->probe_ok = c->stream_ok; c->probed = 1; } pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return len; } static int xferinfo_callback(void *userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { CurlContext *c = userdata; int aborted; pthread_mutex_lock(&c->mutex); aborted = c->aborted; pthread_mutex_unlock(&c->mutex); return aborted; /* non-zero aborts the transfer */ } /* (Re)issue the request for the current offset and add it to the multi. Loop * thread only. */ static void start_request(CurlContext *c) { char range[48]; const char *range_value = NULL; CURLcode cc; int ret; /* At or past the end of the file, report EOF without a request. */ if (c->probed && c->seekable && ((c->content_size >= 0 && c->request_start >= c->content_size) || (c->end_off > 0 && c->request_start >= c->end_off))) { pthread_mutex_lock(&c->mutex); update_status_locked(c, AVERROR_EOF, CURLE_OK); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; } if (!c->probed || c->seekable) { int64_t start = c->request_start; int64_t request_size = c->request_size; if (c->is_initial && c->initial_request_size > 0) request_size = c->initial_request_size; if (request_size > 0 || c->end_off > 0) { int64_t end = INT64_MAX; if (request_size > 0 && start <= INT64_MAX - request_size) end = start + request_size - 1; if (c->content_size > 0) end = FFMIN(end, c->content_size - 1); if (c->end_off > 0) end = FFMIN(end, c->end_off - 1); snprintf(range, sizeof(range), "%"PRId64"-%"PRId64, start, end); } else { snprintf(range, sizeof(range), "%"PRId64"-", start); } range_value = range; } cc = curl_easy_setopt(c->easy, CURLOPT_RANGE, range_value); if ((ret = curl_setopt_checked(c, "CURLOPT_RANGE", cc)) < 0) { c->loop->num_errors++; pthread_mutex_lock(&c->mutex); update_status_locked(c, ret, cc); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; } c->loop->num_requests++; c->request_received = 0; c->request_end = -1; c->active = 1; CURLMcode res = curl_multi_add_handle(c->loop->multi, c->easy); if (res != CURLM_OK) { av_log(c->h, AV_LOG_ERROR, "curl_multi_add_handle: %s\n", curl_multi_strerror(res)); c->active = 0; pthread_mutex_lock(&c->mutex); update_status_locked(c, curlmcode_to_averror(res), curlmcode_to_curlcode(res)); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); } } static void update_statistics(CurlContext *c) { CurlLoop *loop = c->loop; CURL *e = c->easy; curl_off_t recv = 0, time = 0; curl_easy_getinfo(e, CURLINFO_SIZE_DOWNLOAD_T, &recv); curl_easy_getinfo(e, CURLINFO_TOTAL_TIME_T, &time); if (recv) { av_log(c->h, AV_LOG_DEBUG, "%"PRId64" bytes received in %"PRId64" ms\n", (int64_t) recv, (int64_t) time / 1000); loop->total_bytes += recv; loop->total_time_us += time; } long num_conns = 0, num_redirs = 0; curl_easy_getinfo(e, CURLINFO_NUM_CONNECTS, &num_conns); curl_easy_getinfo(e, CURLINFO_REDIRECT_COUNT, &num_redirs); loop->num_connections += (int) num_conns; loop->num_redirects += (int) num_redirs; } /* Transfer finished (or failed) */ static void on_done(CurlContext *c, CURLcode code) { int64_t received; int aborted; pthread_mutex_lock(&c->mutex); aborted = c->aborted; received = c->request_received; /* Advance past delivered bytes so a retry or seek resumes at the right offset. */ if (received > INT64_MAX - c->request_start) { received = 0; aborted = 1; update_status_locked(c, AVERROR(EIO), code); pthread_cond_broadcast(&c->cond); } c->request_start += received; c->request_received = 0; pthread_mutex_unlock(&c->mutex); update_statistics(c); if (!c->probed) { /* Connection died before any usable header arrived. */ pthread_mutex_lock(&c->mutex); c->probed = 1; c->stream_ok = 0; c->loop->num_errors++; update_status_locked(c, curlcode_to_averror(code), code); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; } if (aborted) return; if (c->seek_queued) { /* previous soft seek drain finished; can start new request now */ c->seek_queued = 0; start_request(c); return; } if (code == CURLE_OK && c->stream_ok) { int64_t file_end = c->content_size >= 0 ? c->content_size - 1 : INT64_MAX; if (c->end_off > 0) file_end = FFMIN(file_end, c->end_off - 1); if (c->seekable && c->request_end >= 0 && c->request_end < file_end) { if (!received) { av_log(c->h, AV_LOG_ERROR, "Server sent back no data at offset " "%"PRId64"\n", c->request_start); c->loop->num_errors++; pthread_mutex_lock(&c->mutex); update_status_locked(c, AVERROR(EIO), CURLE_OK); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; } c->is_initial = 0; start_request(c); return; } pthread_mutex_lock(&c->mutex); c->status = AVERROR_EOF; c->curl_status = CURLE_OK; pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; } if (c->stream_ok) { av_log(c->h, AV_LOG_WARNING, "%s\n", curl_easy_strerror(code)); c->loop->num_errors++; } /* Unhandled generic curl error */ pthread_mutex_lock(&c->mutex); update_status_locked(c, curlcode_to_averror(code), code); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); } /* ------------------------------------------------------------------------- */ /* event loop thread + command queue */ /* ------------------------------------------------------------------------- */ static int test_short_seek(CurlContext *c) { if (c->seek_queued) return 1; /* short seek already queued */ if (c->short_seek_size <= 0 || /* short seek disabled */ c->request_end < 0) /* content size not known */ return 0; const int64_t last = c->request_end - c->request_start; return last - c->request_received < c->short_seek_size; } static void execute_command(CurlLoop *loop, CurlCmd *cmd) { CurlContext *c = cmd->ctx; switch (cmd->kind) { case CMD_ADD: start_request(c); break; case CMD_REMOVE: if (c->active) { curl_multi_remove_handle(loop->multi, c->easy); update_statistics(c); c->active = 0; } break; case CMD_UNPAUSE: pthread_mutex_lock(&c->mutex); c->paused = 0; pthread_mutex_unlock(&c->mutex); curl_easy_pause(c->easy, CURLPAUSE_CONT); break; case CMD_SEEK: if (c->active && test_short_seek(c)) { c->seek_queued = 1; } else if (c->active) { curl_multi_remove_handle(loop->multi, c->easy); c->active = 0; } pthread_mutex_lock(&c->mutex); av_fifo_reset2(c->fifo); const int was_paused = c->paused; c->aborted = 0; c->paused = 0; c->status = 0; c->curl_status = 0; pthread_mutex_unlock(&c->mutex); c->request_start = cmd->pos; c->request_received = 0; if (!c->seek_queued) start_request(c); else if (was_paused) curl_easy_pause(c->easy, CURLPAUSE_CONT); break; } } static void *curl_worker(void *arg) { CurlLoop *loop = arg; ff_thread_setname("curl"); while (1) { CurlCmd *cmd; CURLMsg *msg; int running = 0, left = 0, do_exit; pthread_mutex_lock(&loop->mutex); cmd = loop->cmd_head; if (cmd) { loop->cmd_head = cmd->next; if (!loop->cmd_head) loop->cmd_tail = NULL; } do_exit = loop->exit; pthread_mutex_unlock(&loop->mutex); if (cmd) { execute_command(loop, cmd); if (cmd->sync) { pthread_mutex_lock(&loop->mutex); cmd->done = 1; pthread_cond_broadcast(&loop->cond); pthread_mutex_unlock(&loop->mutex); } else { av_free(cmd); } continue; /* drain the whole queue before pumping curl */ } if (do_exit) break; curl_multi_perform(loop->multi, &running); while ((msg = curl_multi_info_read(loop->multi, &left))) { CurlContext *c = NULL; if (msg->msg != CURLMSG_DONE) continue; curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &c); curl_multi_remove_handle(loop->multi, msg->easy_handle); if (c) { c->active = 0; on_done(c, msg->data.result); } } curl_multi_poll(loop->multi, NULL, 0, 1000, NULL); } return NULL; } /* Dispatch a command to the loop. For sync commands the caller blocks until the * loop thread has executed it. Returns 0 or a negative AVERROR. */ static int curl_dispatch(CurlLoop *loop, enum cmd_kind kind, CurlContext *c, int64_t pos, int sync) { CurlCmd stackcmd = {0}; CurlCmd *cmd = sync ? &stackcmd : av_mallocz(sizeof(*cmd)); if (!cmd) return AVERROR(ENOMEM); cmd->kind = kind; cmd->ctx = c; cmd->pos = pos; cmd->sync = sync; pthread_mutex_lock(&loop->mutex); if (loop->cmd_tail) loop->cmd_tail->next = cmd; else loop->cmd_head = cmd; loop->cmd_tail = cmd; curl_multi_wakeup(loop->multi); if (sync) { while (!cmd->done) pthread_cond_wait(&loop->cond, &loop->mutex); } pthread_mutex_unlock(&loop->mutex); return 0; } static void share_lock_callback(CURL *handle, curl_lock_data data, curl_lock_access access, void *userdata) { CurlLoop *loop = userdata; pthread_mutex_lock(&loop->mutex); } static void share_unlock_callback(CURL *handle, curl_lock_data data, void *userdata) { CurlLoop *loop = userdata; pthread_mutex_unlock(&loop->mutex); } static CurlLoop *curl_loop_create(AVFormatContext *avfc) { CurlLoop *loop = av_mallocz(sizeof(*loop)); CURLSHcode res; if (!loop) return NULL; loop->avfc = avfc; if (pthread_mutex_init(&loop->mutex, NULL)) goto fail; if (pthread_cond_init(&loop->cond, NULL)) { pthread_mutex_destroy(&loop->mutex); goto fail; } if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) goto fail2; loop->multi = curl_multi_init(); if (!loop->multi) goto fail3; if (curl_multi_setopt(loop->multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX) != CURLM_OK) goto fail3; loop->share = curl_share_init(); if (!loop->share) goto fail3; if (curl_share_setopt(loop->share, CURLSHOPT_USERDATA, loop) != CURLSHE_OK || curl_share_setopt(loop->share, CURLSHOPT_LOCKFUNC, share_lock_callback) != CURLSHE_OK || curl_share_setopt(loop->share, CURLSHOPT_UNLOCKFUNC, share_unlock_callback) != CURLSHE_OK) goto fail3; res = curl_share_setopt(loop->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); if (res != CURLSHE_OK && res != CURLSHE_NOT_BUILT_IN) goto fail3; loop->has_cookies = res == CURLSHE_OK; res = curl_share_setopt(loop->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS); if (res != CURLSHE_OK && res != CURLSHE_NOT_BUILT_IN) goto fail3; if (pthread_create(&loop->thread, NULL, curl_worker, loop)) goto fail3; return loop; fail3: curl_multi_cleanup(loop->multi); curl_share_cleanup(loop->share); curl_global_cleanup(); fail2: pthread_cond_destroy(&loop->cond); pthread_mutex_destroy(&loop->mutex); fail: av_free(loop); return NULL; } static void print_statistics(CurlLoop *loop) { AVFormatContext *avfc = loop->avfc; if (loop->total_bytes) { double time = loop->total_time_us / 1000000.0; double avg = time ? loop->total_bytes / time : 0; av_log(avfc, AV_LOG_VERBOSE, "libcurl: Overall %"PRId64" bytes received in %.0f ms = %.0f kB/s\n", loop->total_bytes, time * 1e3, avg / 1e3); } if (loop->num_connections || loop->num_errors) { av_log(avfc, AV_LOG_VERBOSE, "libcurl: %d connections, %d redirects, %d requests, %d errors\n", loop->num_connections, loop->num_redirects, loop->num_requests, loop->num_errors); } } static void curl_loop_destroy(CurlLoop *loop) { pthread_mutex_lock(&loop->mutex); loop->exit = 1; curl_multi_wakeup(loop->multi); pthread_mutex_unlock(&loop->mutex); pthread_join(loop->thread, NULL); print_statistics(loop); curl_multi_cleanup(loop->multi); curl_share_cleanup(loop->share); pthread_cond_destroy(&loop->cond); pthread_mutex_destroy(&loop->mutex); av_free(loop); /* Released after the thread is joined and the multi handle is gone. */ curl_global_cleanup(); } /* Attach a context to its event loop. With an owning AVFormatContext the loop is * created lazily, cached on it, and shared across the demuxer's transfers so curl * reuses connections; it is freed at format teardown. Without one the context * gets a private loop freed on close. */ static int curl_loop_attach(CurlContext *c, AVFormatContext *avfc) { if (!avfc) { c->loop = curl_loop_create(NULL); c->private_loop = 1; return c->loop ? 0 : AVERROR(ENOMEM); } pthread_mutex_lock(&curl_loop_lock); c->loop = ffformatcontext(avfc)->curl_loop; if (!c->loop) { c->loop = curl_loop_create(avfc); ffformatcontext(avfc)->curl_loop = c->loop; } pthread_mutex_unlock(&curl_loop_lock); return c->loop ? 0 : AVERROR(ENOMEM); } void ff_curl_loop_free(struct CurlLoop **loop) { if (loop && *loop) { curl_loop_destroy(*loop); *loop = NULL; } } /* ------------------------------------------------------------------------- */ /* URLProtocol callbacks */ /* ------------------------------------------------------------------------- */ static int libcurl_close(URLContext *h); static int debug_callback(CURL *easy, curl_infotype type, char *data, size_t size, void *userdata) { CurlContext *c = userdata; const char *prefix, *p = data, *end = data + size; switch (type) { case CURLINFO_TEXT: prefix = "* "; break; case CURLINFO_HEADER_IN: prefix = "< "; break; case CURLINFO_HEADER_OUT: prefix = "> "; break; default: return 0; } /* Split multiline payload into each log. */ while (p < end) { const char *nl = memchr(p, '\n', end - p); size_t len = (nl ? nl : end) - p; while (len && p[len - 1] == '\r') len--; av_log(c->h, AV_LOG_DEBUG, "%s%.*s\n", prefix, (int)len, p); if (!nl) break; p = nl + 1; } return 0; } static int slist_append(struct curl_slist **list, const char *str) { struct curl_slist *tmp = curl_slist_append(*list, str); if (!tmp) return AVERROR(ENOMEM); *list = tmp; return 0; } /* Build the custom request header list from the referer and headers options. */ static int build_headers(CurlContext *c) { int user_set_icy = 0, ret = 0; if (c->referer && c->referer[0]) { char *h = av_asprintf("Referer: %s", c->referer); if (!h) return AVERROR(ENOMEM); ret = slist_append(&c->header_list, h); av_free(h); if (ret < 0) return ret; } if (c->headers && c->headers[0]) { char *copy = av_strdup(c->headers); char *line, *saveptr = NULL; if (!copy) return AVERROR(ENOMEM); for (line = av_strtok(copy, "\r\n", &saveptr); line && ret >= 0; line = av_strtok(NULL, "\r\n", &saveptr)) { if (!av_strncasecmp(line, "Icy-MetaData:", 13)) user_set_icy = 1; ret = slist_append(&c->header_list, line); } av_free(copy); if (ret < 0) return ret; } /* libcurl does not deduplicate the list, so only add ours if the user * did not already ask for one. */ if (c->icy && !user_set_icy) return slist_append(&c->header_list, "Icy-MetaData: 1"); return 0; } static int setup_protocols(CurlContext *c) { const char *wl = c->h->protocol_whitelist; const char *bl = c->h->protocol_blacklist; int ret; if (!wl && !bl) return 0; AVBPrint bp; av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC); curl_version_info_data *info = curl_version_info(CURLVERSION_NOW); for (const char *const *p = info->protocols; *p; p++) { const char *proto = *p; if (av_strcasecmp(proto, "http") && av_strcasecmp(proto, "https")) continue; /* only http(s) are supported by libcurl.c at the moment */ if (wl && av_match_list(proto, wl, ',') <= 0) continue; if (bl && av_match_list(proto, bl, ',') > 0) continue; if (bp.len) av_bprint_chars(&bp, ',', 1); av_bprintf(&bp, "%s", proto); } if (!av_bprint_is_complete(&bp)) { av_bprint_finalize(&bp, NULL); return AVERROR(ENOMEM); } if (!bp.len) { av_log(c->h, AV_LOG_ERROR, "Set of allowed protocols is empty.\n"); av_bprint_finalize(&bp, NULL); return AVERROR(EINVAL); } ret = curl_setopt_checked(c, "CURLOPT_PROTOCOLS_STR", curl_easy_setopt(c->easy, CURLOPT_PROTOCOLS_STR, bp.str)); if (!ret) ret = curl_setopt_checked(c, "CURLOPT_REDIR_PROTOCOLS_STR", curl_easy_setopt(c->easy, CURLOPT_REDIR_PROTOCOLS_STR, bp.str)); av_bprint_finalize(&bp, NULL); return ret; } #define CURL_SETOPT(opt, val) \ do { \ int ret_ = curl_setopt_checked(c, #opt, \ curl_easy_setopt(e, opt, val)); \ if (ret_ < 0) \ return ret_; \ } while (0) static int setup_curl(CurlContext *c) { const curl_version_info_data *info = curl_version_info(CURLVERSION_NOW); CURL *e = c->easy; CURLcode cc; int ret; const char *url = c->h->filename; /* Drop an optional "libcurl:" prefix that forces this protocol. */ av_strstart(url, "libcurl:", &url); CURL_SETOPT(CURLOPT_URL, url); CURL_SETOPT(CURLOPT_PRIVATE, c); CURL_SETOPT(CURLOPT_NOSIGNAL, 1L); CURL_SETOPT(CURLOPT_SHARE, c->loop->share); CURL_SETOPT(CURLOPT_WRITEFUNCTION, write_callback); CURL_SETOPT(CURLOPT_WRITEDATA, c); CURL_SETOPT(CURLOPT_HEADERFUNCTION, header_callback); CURL_SETOPT(CURLOPT_HEADERDATA, c); CURL_SETOPT(CURLOPT_NOPROGRESS, 0L); CURL_SETOPT(CURLOPT_XFERINFOFUNCTION, xferinfo_callback); CURL_SETOPT(CURLOPT_XFERINFODATA, c); if (av_log_get_level() >= AV_LOG_DEBUG) { CURL_SETOPT(CURLOPT_VERBOSE, 1L); CURL_SETOPT(CURLOPT_DEBUGFUNCTION, debug_callback); CURL_SETOPT(CURLOPT_DEBUGDATA, c); } CURL_SETOPT(CURLOPT_FOLLOWLOCATION, 1L); CURL_SETOPT(CURLOPT_MAXREDIRS, (long)c->max_redirects); CURL_SETOPT(CURLOPT_HTTP_VERSION, (long)c->http_version); CURL_SETOPT(CURLOPT_TCP_KEEPALIVE, c->multiple_requests ? 1L : 0L); CURL_SETOPT(CURLOPT_FORBID_REUSE, c->multiple_requests ? 0L : 1L); if (info->features & CURL_VERSION_HSTS) CURL_SETOPT(CURLOPT_HSTS_CTRL, (long)CURLHSTS_ENABLE); CURL_SETOPT(CURLOPT_ACCEPT_ENCODING, c->off > 0 || c->end_off > 0 ? "identity" : ""); if (c->connect_timeout > 0) CURL_SETOPT(CURLOPT_CONNECTTIMEOUT_MS, (long)c->connect_timeout * 1000); if (c->user_agent && c->user_agent[0]) CURL_SETOPT(CURLOPT_USERAGENT, c->user_agent); if (c->http_proxy && c->http_proxy[0]) CURL_SETOPT(CURLOPT_PROXY, c->http_proxy); if (info->features & CURL_VERSION_SSL) CURL_SETOPT(CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NATIVE_CA); CURL_SETOPT(CURLOPT_SSL_VERIFYPEER, c->tls_verify ? 1L : 0L); CURL_SETOPT(CURLOPT_SSL_VERIFYHOST, c->tls_verify ? 2L : 0L); if (c->ca_file) CURL_SETOPT(CURLOPT_CAINFO, c->ca_file); if (c->cert_file) CURL_SETOPT(CURLOPT_SSLCERT, c->cert_file); if (c->key_file) CURL_SETOPT(CURLOPT_SSLKEY, c->key_file); if (c->loop->has_cookies) { CURL_SETOPT(CURLOPT_COOKIEFILE, ""); } else if (c->cookies && c->cookies[0]) { av_log(c->h, AV_LOG_ERROR, "libcurl was built without cookie support\n"); return AVERROR(ENOSYS); } if (c->cookies && c->cookies[0]) { char *copy = av_strdup(c->cookies); char *line, *saveptr = NULL; if (!copy) return AVERROR(ENOMEM); for (line = av_strtok(copy, "\r\n", &saveptr); line; line = av_strtok(NULL, "\r\n", &saveptr)) { char *sc = av_asprintf("Set-Cookie: %s", line); if (!sc) { av_free(copy); return AVERROR(ENOMEM); } cc = curl_easy_setopt(e, CURLOPT_COOKIELIST, sc); av_free(sc); if ((ret = curl_setopt_checked(c, "CURLOPT_COOKIELIST", cc)) < 0) { av_free(copy); return ret; } } av_free(copy); } if ((ret = build_headers(c)) < 0) return ret; if (c->header_list) CURL_SETOPT(CURLOPT_HTTPHEADER, c->header_list); /* Shoutcast v1 answers "ICY 200 OK", which curl would otherwise reject as * HTTP/0.9 before any header reaches header_callback(). */ if (c->icy_status) { if ((ret = slist_append(&c->alias_list, "ICY 200")) < 0) return ret; CURL_SETOPT(CURLOPT_HTTP200ALIASES, c->alias_list); } return 0; } #undef CURL_SETOPT static void curl_cond_wait(CurlContext *c) { int64_t t = av_gettime() + CURL_WAIT_US; struct timespec ts = { .tv_sec = t / 1000000, .tv_nsec = (t % 1000000) * 1000 }; pthread_cond_timedwait(&c->cond, &c->mutex, &ts); } /* Block until the transfer has been probed, the stream errored, or the open was * interrupted. Returns 0, or a negative AVERROR. */ static int wait_for_probe(CurlContext *c) { URLContext *h = c->h; int ret = 0; pthread_mutex_lock(&c->mutex); while (!c->probed && !c->status) { if (ff_check_interrupt(&h->interrupt_callback)) { c->aborted = 1; ret = AVERROR_EXIT; break; } curl_cond_wait(c); } if (!ret) { if (!c->probe_ok) ret = c->status ? c->status : AVERROR(EIO); } pthread_mutex_unlock(&c->mutex); return ret; } /* Scales by the recurrence relationship x := 2x + 1, i.e. 2^n - 1 */ static int64_t retry_delay(CurlContext *c) { int64_t factor = INT64_MAX >> FFMAX(63 - c->retry_count, 0); if (factor >= CURL_RETRY_MAX_US / CURL_RETRY_BASE_US) return CURL_RETRY_MAX_US; return factor * CURL_RETRY_BASE_US; } static int retry_request_locked(URLContext *h, int nonblock) { CurlContext *c = h->priv_data; const int status = c->status; const CURLcode code = c->curl_status; pthread_mutex_unlock(&c->mutex); if (!c->retry_time) { if (c->retry_count >= c->max_retries) { av_log(h, AV_LOG_ERROR, "Maximum number of retries (%d) reached\n", c->max_retries); return status; } const int64_t delay = retry_delay(c); c->retry_time = av_gettime_relative() + delay; c->retry_count++; av_log(h, AV_LOG_WARNING, "Retrying (#%d) from %"PRId64" in %.3fs " "after: %s (%s)\n", c->retry_count, c->logical_pos, delay * 1e-6, av_err2str(status), curl_easy_strerror(code)); } const int64_t sleep_us = c->retry_time - av_gettime_relative(); if (sleep_us > 0 && !nonblock) { av_usleep(FFMIN(sleep_us, CURL_WAIT_US)); } else if (sleep_us <= 0) { /* Use a synchronous request to ensure that the seek is registered, and * the reset of c->state is observable, before the next libcurl_read() * call, otherwise this might hit the exact same retry path a second * time. */ int ret = curl_dispatch(c->loop, CMD_SEEK, c, c->logical_pos, 1); if (ret < 0) return ret; c->retry_time = 0; } return AVERROR(EAGAIN); /* allow caller to handle interrupts and retry */ } static int libcurl_open(URLContext *h, const char *url, int flags, AVDictionary **options) { /* Guard against non-thread-safe libcurl builds. This should never happen, * since libcurl is used only on platforms with thread support, and thread * safety is enabled unconditionally in libcurl when the platform supports * threads or atomics. */ curl_version_info_data *info = curl_version_info(CURLVERSION_NOW); if (!(info->features & CURL_VERSION_THREADSAFE)) return AVERROR(ENOSYS); CurlContext *c = h->priv_data; const char *eff_url = h->filename; int ret; c->h = h; c->content_size = -1; c->request_start = c->off; c->request_end = -1; c->logical_pos = c->off; c->is_initial = 1; c->icy_block_len = -1; /* Report the request URL until header_callback replaces it post-redirect. */ av_strstart(eff_url, "libcurl:", &eff_url); av_freep(&c->location); c->location = av_strdup(eff_url); ret = pthread_mutex_init(&c->mutex, NULL); if (ret) return AVERROR(ret); ret = pthread_cond_init(&c->cond, NULL); if (ret) { pthread_mutex_destroy(&c->mutex); return AVERROR(ret); } c->fifo = av_fifo_alloc2(c->buffer_size, 1, 0); if (!c->fifo) { ret = AVERROR(ENOMEM); goto fail; } ret = curl_loop_attach(c, h->avfc); if (ret < 0) goto fail; c->easy = curl_easy_init(); if (!c->easy) { ret = AVERROR(ENOMEM); goto fail; } ret = setup_protocols(c); if (ret < 0) goto fail; ret = setup_curl(c); if (ret < 0) goto fail; ret = curl_dispatch(c->loop, CMD_ADD, c, 0, 0); if (ret < 0) goto fail; ret = wait_for_probe(c); if (ret < 0) goto fail; pthread_mutex_lock(&c->mutex); h->is_streamed = !c->seekable; pthread_mutex_unlock(&c->mutex); return 0; fail: libcurl_close(h); return ret; } /* Export the metadata block, which the packet parser splits in place. */ static int update_icy_metadata(CurlContext *c) { av_freep(&c->icy_metadata_packet); c->icy_metadata_packet = av_strdup((char *)c->icy_block); if (!c->icy_metadata_packet) return AVERROR(ENOMEM); ff_http_parse_icy_packet(c->h, &c->metadata, (char *)c->icy_block); return 0; } /* Consume the metadata block at the current interleave boundary, with the * mutex held. Returns 1 once a block is complete and ready to export. */ static int drain_icy_block(CurlContext *c) { size_t avail; while ((avail = av_fifo_can_read(c->fifo))) { int n; if (c->icy_block_len < 0) { uint8_t units; av_fifo_read(c->fifo, &units, 1); c->icy_block_len = units * 16; c->icy_block_filled = 0; /* A zero length byte means the metadata did not change. */ if (!c->icy_block_len) { c->icy_block_len = -1; c->icy_data_read = 0; return 0; } continue; } n = FFMIN(avail, (size_t)(c->icy_block_len - c->icy_block_filled)); av_fifo_read(c->fifo, c->icy_block + c->icy_block_filled, n); c->icy_block_filled += n; if (c->icy_block_filled < c->icy_block_len) return 0; c->icy_block[c->icy_block_len] = 0; c->icy_block_len = -1; c->icy_data_read = 0; return 1; } return 0; } static int libcurl_read(URLContext *h, unsigned char *buf, int size) { CurlContext *c = h->priv_data; int nonblock = h->flags & AVIO_FLAG_NONBLOCK; int unpause, ret; pthread_mutex_lock(&c->mutex); while (1) { size_t avail; /* A failed export loses one metadata update but leaves the interleave * in sync, since the block is fully consumed either way. */ if (c->icy_metaint > 0 && c->icy_data_read == c->icy_metaint && drain_icy_block(c) && (ret = update_icy_metadata(c)) < 0) break; avail = av_fifo_can_read(c->fifo); if (c->icy_metaint > 0) avail = FFMIN(avail, (size_t)(c->icy_metaint - c->icy_data_read)); if (avail) { ret = FFMIN(avail, (size_t)size); av_fifo_read(c->fifo, buf, ret); c->icy_data_read += ret; c->logical_pos += ret; c->retry_count = 0; c->retry_time = 0; break; } if (c->status) { if (c->seekable && is_recoverable(c->curl_status)) return retry_request_locked(h, nonblock); if (c->status == AVERROR_EOF && c->icy_block_len >= 0) av_log(h, AV_LOG_WARNING, "Stream ended inside an ICY metadata block\n"); ret = c->status; break; } if (nonblock) { ret = AVERROR(EAGAIN); break; } curl_cond_wait(c); /* Return to the avio layer so it can poll the interrupt callback. */ nonblock = 1; } /* Resume a paused transfer once the FIFO is at least half empty, on every * exit path because ICY framing can be drained without returning media. */ unpause = c->paused && av_fifo_can_write(c->fifo) * 2 >= c->buffer_size; pthread_mutex_unlock(&c->mutex); if (unpause) curl_dispatch(c->loop, CMD_UNPAUSE, c, 0, 0); return ret; } static int64_t libcurl_seek(URLContext *h, int64_t pos, int whence) { CurlContext *c = h->priv_data; int64_t newpos; pthread_mutex_lock(&c->mutex); const int64_t content_size = c->content_size; const int seekable = c->seekable; pthread_mutex_unlock(&c->mutex); if (whence == AVSEEK_SIZE) return content_size >= 0 ? content_size : AVERROR(ENOSYS); if (!seekable) return AVERROR(ENOSYS); switch (whence) { case SEEK_SET: newpos = pos; break; case SEEK_CUR: if (pos > INT64_MAX - c->logical_pos) return AVERROR(ERANGE); newpos = c->logical_pos + pos; break; case SEEK_END: if (content_size < 0) return AVERROR(ENOSYS); if (pos > INT64_MAX - content_size) return AVERROR(ERANGE); newpos = content_size + pos; break; default: return AVERROR(EINVAL); } if (newpos < 0) return AVERROR(EINVAL); if (newpos == c->logical_pos) return newpos; /* Restart the transfer at the new offset. Any failure of the new request * surfaces on the following url_read(). */ curl_dispatch(c->loop, CMD_SEEK, c, newpos, 1); c->logical_pos = newpos; c->retry_count = 0; c->retry_time = 0; return newpos; } static int libcurl_close(URLContext *h) { CurlContext *c = h->priv_data; if (c->loop) { if (c->easy) { /* Ensure the handle is out of the multi before we free it. */ curl_dispatch(c->loop, CMD_REMOVE, c, 0, 1); curl_easy_cleanup(c->easy); c->easy = NULL; } /* A shared loop outlives the transfer for connection reuse. */ if (c->private_loop) curl_loop_destroy(c->loop); c->loop = NULL; } if (c->header_list) curl_slist_free_all(c->header_list); if (c->alias_list) curl_slist_free_all(c->alias_list); av_dict_free(&c->hdr_icy); av_fifo_freep2(&c->fifo); pthread_cond_destroy(&c->cond); pthread_mutex_destroy(&c->mutex); return 0; } static int libcurl_get_short_seek(URLContext *h) { CurlContext *c = h->priv_data; if (c->short_seek_size >= 1) return FFMIN(c->short_seek_size, INT_MAX); return AVERROR(ENOSYS); } #define OFFSET(x) offsetof(CurlContext, x) #define D AV_OPT_FLAG_DECODING_PARAM #define E AV_OPT_FLAG_ENCODING_PARAM static const AVOption options[] = { { "user_agent", "override User-Agent header", OFFSET(user_agent), AV_OPT_TYPE_STRING, { .str = DEFAULT_USER_AGENT }, 0, 0, D }, { "referer", "override Referer header", OFFSET(referer), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D }, { "headers", "set custom HTTP headers, can override built in default headers", OFFSET(headers), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E }, { "http_proxy", "set HTTP proxy to tunnel through", OFFSET(http_proxy), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E }, { "cookies", "set cookies to be sent in applicable future requests, use newline delimited Set-Cookie HTTP field value syntax", OFFSET(cookies), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D }, { "location", "the actual location of the data received", OFFSET(location), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E }, { "offset", "initial byte offset", OFFSET(off), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { "end_offset", "try to limit the request to bytes preceding this offset", OFFSET(end_off), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { "seekable", "control seekability of connection", OFFSET(seekable_opt), AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, D }, { "tls_verify", "verify the peer certificate and hostname", OFFSET(tls_verify), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D | E }, { "ca_file", "certificate authority bundle file", OFFSET(ca_file), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E }, { "cert_file", "client certificate file", OFFSET(cert_file), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E }, { "key_file", "client private key file", OFFSET(key_file), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E }, { "connect_timeout", "connection timeout in seconds (0 = libcurl default)", OFFSET(connect_timeout), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX / 1000, D | E }, { "max_redirects", "maximum number of redirects to follow", OFFSET(max_redirects), AV_OPT_TYPE_INT, { .i64 = 16 }, 0, INT_MAX, D }, { "multiple_requests", "reuse the connection across requests (HTTP keep-alive)", OFFSET(multiple_requests), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D | E }, { "max_retries", "maximum number of retries after a recoverable error", OFFSET(max_retries), AV_OPT_TYPE_INT, { .i64 = 5 }, 0, INT_MAX, D }, { "icy", "request ICY metadata", OFFSET(icy), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D }, { "icy_status", "accept legacy Shoutcast \"ICY 200 OK\" status lines", OFFSET(icy_status), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D }, { "icy_metadata_headers", "return ICY metadata headers", OFFSET(icy_metadata_headers), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_EXPORT }, { "icy_metadata_packet", "return current ICY metadata packet", OFFSET(icy_metadata_packet), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_EXPORT }, { "metadata", "metadata read from the bitstream", OFFSET(metadata), AV_OPT_TYPE_DICT, {0}, 0, 0, AV_OPT_FLAG_EXPORT }, { "buffer_size", "receive buffer size in bytes", OFFSET(buffer_size), AV_OPT_TYPE_INT64, { .i64 = CURL_DEFAULT_BUFFER_SIZE }, CURL_MAX_WRITE_SIZE, INT_MAX, D }, { "request_size", "split a transfer into ranged requests of at most this many bytes (0 = unlimited)", OFFSET(request_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { "initial_request_size", "size (in bytes) of initial requests made during probing / header parsing", OFFSET(initial_request_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { "http_version", "HTTP version to use", OFFSET(http_version), AV_OPT_TYPE_INT, { .i64 = CURL_HTTP_VERSION_NONE }, 0, INT_MAX, D, .unit = "http_version" }, { "auto", "negotiate the best supported version", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_NONE }, 0, 0, D, .unit = "http_version" }, { "1.0", "HTTP/1.0", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_1_0 }, 0, 0, D, .unit = "http_version" }, { "1.1", "HTTP/1.1", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_1_1 }, 0, 0, D, .unit = "http_version" }, { "2", "HTTP/2", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_2 }, 0, 0, D, .unit = "http_version" }, { "2tls", "HTTP/2 over TLS only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_2TLS }, 0, 0, D, .unit = "http_version" }, { "2-prior-knowledge", "HTTP/2 without an upgrade handshake", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE }, 0, 0, D, .unit = "http_version" }, { "3", "HTTP/3, fall back to earlier versions", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3 }, 0, 0, D, .unit = "http_version" }, { "3only", "HTTP/3 only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3ONLY }, 0, 0, D, .unit = "http_version" }, { "short_seek_size", "threshold to favor readahead over seek", OFFSET(short_seek_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { NULL } }; static const AVClass libcurl_context_class = { .class_name = "libcurl", .item_name = av_default_item_name, .option = options, .version = LIBAVUTIL_VERSION_INT, }; const URLProtocol ff_libcurl_protocol = { .name = "libcurl", .url_open2 = libcurl_open, .url_read = libcurl_read, .url_seek = libcurl_seek, .url_close = libcurl_close, .url_get_short_seek = libcurl_get_short_seek, .priv_data_size = sizeof(CurlContext), .priv_data_class = &libcurl_context_class, .flags = URL_PROTOCOL_FLAG_NETWORK, .default_whitelist = "http,https,libcurl", };