This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

commit d0972a725d1f91f069a5e044d220fe45a573bb3e
Author:     Kacper Michajłow <[email protected]>
AuthorDate: Mon Jun 15 03:34:03 2026 +0200
Commit:     Kacper Michajłow <[email protected]>
CommitDate: Mon Jul 27 17:05:20 2026 +0000

    avformat/libcurl: add options, TLS, proxy, cookies and headers
    
    Add the AVOption table and map it onto libcurl. Verbose curl output is
    routed to av_log at debug level. This mirrors subset of http.c options.
    
    Signed-off-by: Kacper Michajłow <[email protected]>
---
 doc/protocols.texi    |  87 +++++++++++++++++++
 libavformat/libcurl.c | 227 ++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 305 insertions(+), 9 deletions(-)

diff --git a/doc/protocols.texi b/doc/protocols.texi
index d51c12d6f8..fb0f19c8b4 100644
--- a/doc/protocols.texi
+++ b/doc/protocols.texi
@@ -1061,6 +1061,93 @@ The Real-Time Messaging Protocol tunneled through HTTPS 
(RTMPTS) is used
 for streaming multimedia content within HTTPS requests to traverse
 firewalls.
 
+@section libcurl
+
+HTTP and HTTPS access through the libcurl library, available as an alternative 
to
+the native @code{http} protocol when FFmpeg is built with 
@code{--enable-libcurl}.
+It runs transfers on a dedicated event-loop thread shared per
+@code{AVFormatContext}, so all of a demuxer's connections (for example HLS
+segments) reuse libcurl's connection cache and HTTP/2 and HTTP/3 multiplexing.
+
+The native @code{http} protocol stays the default; to use libcurl explicitly,
+prefix the URL with @code{libcurl:}, for example
+@code{libcurl:https://example.com/video.mp4}.
+
+The libcurl protocol accepts the following options.
+
+@table @option
+@item user_agent
+Override the @code{User-Agent} header. Defaults to @code{Lavf/<version>}.
+
+@item referer
+Set the @code{Referer} header.
+
+@item headers
+Set custom HTTP headers, overriding internal defaults. Multiple headers are
+separated by newlines.
+
+@item http_proxy
+Set the HTTP proxy to tunnel through.
+
+@item cookies
+Cookies to be sent in future requests, given as newline-delimited
+@code{Set-Cookie}-style values.
+
+@item location
+The actual location of the data after following redirects. This is an output
+option, read with @command{av_opt_get}; demuxers use it to resolve relative 
URLs.
+
+@item offset
+Initial byte offset of the request. Default is @code{0}.
+
+@item end_offset
+If greater than @code{0}, limit the request to the bytes preceding this offset.
+Together with @option{offset} this requests a sub-range of the resource, as 
used
+for HLS byte-range segments.
+
+@item seekable
+Control seekability of the connection. If set to @code{0} the stream is treated
+as non-seekable, @code{1} forces seekable, and @code{-1} (the default) lets it 
be
+detected from the server response.
+
+@item tls_verify
+Verify the peer certificate and host name. Enabled by default.
+
+@item ca_file
+Path to a certificate authority bundle that overrides the system store.
+
+@item cert_file
+@item key_file
+Client certificate and private key files for mutual TLS.
+
+@item connect_timeout
+Connection timeout in seconds. @code{0} (the default) uses libcurl's own 
timeout.
+
+@item max_redirects
+Maximum number of redirects to follow. Default is @code{16}.
+
+@item multiple_requests
+Reuse the connection across requests (HTTP keep-alive). Enabled by default;
+setting it to @code{0} uses a fresh connection per transfer.
+
+@item http_version
+HTTP version to use: @code{auto} (default), @code{1.0}, @code{1.1}, @code{2},
+@code{2tls}, @code{2-prior-knowledge}, @code{3} or @code{3only}.
+
+@item buffer_size
+Size in bytes of the receive buffer. Default is 4 MiB.
+
+@item request_size
+If greater than @code{0}, split a seekable transfer into ranged requests of at
+most this many bytes each. Default is @code{0} (a single request).
+
+@item max_retries
+Maximum number of retries after a recoverable error on a seekable transfer.
+Default is @code{5}.
+@end table
+
+For more information see: @url{https://curl.se/libcurl/}.
+
 @section libsmbclient
 
 libsmbclient permits one to manipulate CIFS/SMB network resources.
diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c
index 92e5250c66..6cf92b3d42 100644
--- a/libavformat/libcurl.c
+++ b/libavformat/libcurl.c
@@ -23,12 +23,14 @@
 
 #include <curl/curl.h>
 #include <inttypes.h>
+#include <limits.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include "libavutil/avstring.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"
@@ -39,8 +41,11 @@
 #include "http.h"
 #include "internal.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
@@ -80,8 +85,28 @@ struct CurlContext {
     CurlLoop       *loop;
     int             private_loop;  /* loop is owned by this context (not 
shared) */
     CURL           *easy;
-
+    struct curl_slist *header_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;
     int             max_retries;
 
     int64_t         logical_pos; /* next byte url_read() will return, caller 
side */
@@ -239,6 +264,19 @@ static size_t header_callback(char *ptr, size_t size, 
size_t nitems, void *userd
     pthread_mutex_lock(&c->mutex);
     if (status >= 200 && status < 300) {
         c->stream_ok = 1;
+        /* 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;
+                }
+            }
+        }
         /* A compressed body is addressed in encoded form, so byte offsets are
          * meaningless: not seekable. Note that we prefer compression over
          * seekability, servers doesn't offer media in compressed form, so it
@@ -283,8 +321,20 @@ static int xferinfo_callback(void *userdata, curl_off_t 
dltotal, curl_off_t dlno
 static void start_request(CurlContext *c)
 {
     if (!c->probed || c->seekable) {
-        char range[32];
-        snprintf(range, sizeof(range), "%"PRIu64"-", c->request_start);
+        uint64_t start = c->request_start;
+        char range[48];
+        if (c->request_size > 0 || c->end_off > 0) {
+            uint64_t end = UINT64_MAX;
+            if (c->request_size > 0)
+                end = start + c->request_size - 1;
+            if (c->content_size > 0)
+                end = FFMIN(end, (uint64_t)c->content_size - 1);
+            if (c->end_off > 0)
+                end = FFMIN(end, (uint64_t)c->end_off - 1);
+            snprintf(range, sizeof(range), "%"PRIu64"-%"PRIu64, start, end);
+        } else {
+            snprintf(range, sizeof(range), "%"PRIu64"-", start);
+        }
         curl_easy_setopt(c->easy, CURLOPT_RANGE, range);
     } else {
         curl_easy_setopt(c->easy, CURLOPT_RANGE, NULL);
@@ -307,12 +357,14 @@ static void start_request(CurlContext *c)
 /* Transfer finished (or failed) */
 static void on_done(CurlContext *c, CURLcode code)
 {
+    uint64_t received;
     int aborted;
 
     pthread_mutex_lock(&c->mutex);
-    aborted = c->aborted;
+    aborted  = c->aborted;
+    received = c->request_received;
     /* Advance past delivered bytes so a retry or seek resumes at the right 
offset. */
-    c->request_start    += c->request_received;
+    c->request_start    += received;
     c->request_received  = 0;
     pthread_mutex_unlock(&c->mutex);
 
@@ -329,6 +381,17 @@ static void on_done(CurlContext *c, CURLcode code)
     }
 
     if (code == CURLE_OK && !aborted && c->stream_ok) {
+        c->retry_count = 0;
+        if (c->seekable && c->request_size > 0) {
+            int64_t file_end = c->end_off > 0 ? c->end_off : c->content_size;
+            int more = file_end > 0
+                       ? (int64_t)c->request_start < file_end
+                       : received >= (uint64_t)c->request_size;
+            if (more) {
+                start_request(c);
+                return;
+            }
+        }
         pthread_mutex_lock(&c->mutex);
         c->eof = 1;
         pthread_cond_broadcast(&c->cond);
@@ -576,6 +639,58 @@ void ff_curl_loop_free(struct CurlLoop **loop)
 
 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;
+}
+
+/* Build the custom request header list from the referer and headers options. 
*/
+static struct curl_slist *build_headers(CurlContext *c)
+{
+    struct curl_slist *list = NULL;
+
+    if (c->referer && c->referer[0]) {
+        char *h = av_asprintf("Referer: %s", c->referer);
+        if (h) {
+            list = curl_slist_append(list, h);
+            av_free(h);
+        }
+    }
+    if (c->headers && c->headers[0]) {
+        char *copy = av_strdup(c->headers);
+        char *line, *saveptr = NULL;
+        if (copy) {
+            for (line = av_strtok(copy, "\r\n", &saveptr); line;
+                 line = av_strtok(NULL, "\r\n", &saveptr))
+                list = curl_slist_append(list, line);
+            av_free(copy);
+        }
+    }
+    return list;
+}
+
 static void setup_curl(CurlContext *c)
 {
     CURL *e = c->easy;
@@ -597,9 +712,58 @@ static void setup_curl(CurlContext *c)
     curl_easy_setopt(e, CURLOPT_XFERINFOFUNCTION, xferinfo_callback);
     curl_easy_setopt(e, CURLOPT_XFERINFODATA, c);
 
+    if (av_log_get_level() >= AV_LOG_DEBUG) {
+        curl_easy_setopt(e, CURLOPT_VERBOSE, 1L);
+        curl_easy_setopt(e, CURLOPT_DEBUGFUNCTION, debug_callback);
+        curl_easy_setopt(e, CURLOPT_DEBUGDATA, c);
+    }
+
     curl_easy_setopt(e, CURLOPT_FOLLOWLOCATION, 1L);
-    curl_easy_setopt(e, CURLOPT_TCP_KEEPALIVE, 1L);
+    curl_easy_setopt(e, CURLOPT_MAXREDIRS, (long)c->max_redirects);
+    curl_easy_setopt(e, CURLOPT_HTTP_VERSION, (long)c->http_version);
+    curl_easy_setopt(e, CURLOPT_TCP_KEEPALIVE, c->multiple_requests ? 1L : 0L);
+    curl_easy_setopt(e, CURLOPT_FORBID_REUSE,  c->multiple_requests ? 0L : 1L);
+    curl_easy_setopt(e, CURLOPT_HSTS_CTRL, (long)CURLHSTS_ENABLE);
     curl_easy_setopt(e, CURLOPT_ACCEPT_ENCODING, "");
+    if (c->connect_timeout > 0)
+        curl_easy_setopt(e, CURLOPT_CONNECTTIMEOUT_MS,
+                         (long)c->connect_timeout * 1000);
+
+    if (c->user_agent && c->user_agent[0])
+        curl_easy_setopt(e, CURLOPT_USERAGENT, c->user_agent);
+    if (c->http_proxy && c->http_proxy[0])
+        curl_easy_setopt(e, CURLOPT_PROXY, c->http_proxy);
+
+    curl_easy_setopt(e, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NATIVE_CA);
+    curl_easy_setopt(e, CURLOPT_SSL_VERIFYPEER, c->tls_verify ? 1L : 0L);
+    curl_easy_setopt(e, CURLOPT_SSL_VERIFYHOST, c->tls_verify ? 2L : 0L);
+    if (c->ca_file)
+        curl_easy_setopt(e, CURLOPT_CAINFO, c->ca_file);
+    if (c->cert_file)
+        curl_easy_setopt(e, CURLOPT_SSLCERT, c->cert_file);
+    if (c->key_file)
+        curl_easy_setopt(e, CURLOPT_SSLKEY, c->key_file);
+
+    if (c->cookies && c->cookies[0]) {
+        char *copy = av_strdup(c->cookies);
+        char *line, *saveptr = NULL;
+        curl_easy_setopt(e, CURLOPT_COOKIEFILE, ""); /* enable the cookie 
engine */
+        if (copy) {
+            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) {
+                    curl_easy_setopt(e, CURLOPT_COOKIELIST, sc);
+                    av_free(sc);
+                }
+            }
+            av_free(copy);
+        }
+    }
+
+    c->header_list = build_headers(c);
+    if (c->header_list)
+        curl_easy_setopt(e, CURLOPT_HTTPHEADER, c->header_list);
 }
 
 static void curl_cond_wait(CurlContext *c)
@@ -647,13 +811,18 @@ static int libcurl_open(URLContext *h, const char *url, 
int flags,
         return AVERROR(ENOSYS);
 
     CurlContext *c = h->priv_data;
+    const char *eff_url = h->filename;
     int ret;
 
     c->h = h;
     c->content_size = -1;
-    c->max_retries  = 5;
-    if (c->buffer_size <= 0)
-        c->buffer_size = CURL_DEFAULT_BUFFER_SIZE;
+    c->request_start = c->off;
+    c->logical_pos   = c->off;
+
+    /* 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)
@@ -689,6 +858,8 @@ static int libcurl_open(URLContext *h, const char *url, int 
flags,
     if (ret < 0)
         goto fail;
 
+    if (c->seekable_opt == 0)
+        c->seekable = 0;
     h->is_streamed = !c->seekable;
 
     return 0;
@@ -795,6 +966,8 @@ static int libcurl_close(URLContext *h)
         c->loop = NULL;
     }
 
+    if (c->header_list)
+        curl_slist_free_all(c->header_list);
     av_fifo_freep2(&c->fifo);
     pthread_cond_destroy(&c->cond);
     pthread_mutex_destroy(&c->mutex);
@@ -802,9 +975,45 @@ static int libcurl_close(URLContext *h)
     return 0;
 }
 
+#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, 
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 },
+    { "buffer_size", "receive buffer size in bytes", OFFSET(buffer_size), 
AV_OPT_TYPE_INT64, { .i64 = CURL_DEFAULT_BUFFER_SIZE }, 1024, INT64_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 },
+    { "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" },
+    { NULL }
+};
+
 static const AVClass libcurl_context_class = {
     .class_name = "libcurl",
     .item_name  = av_default_item_name,
+    .option     = options,
     .version    = LIBAVUTIL_VERSION_INT,
 };
 

_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to