2026-06-15 02:30:08 +02:00
/*
* 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 <curl/curl.h>
2026-06-15 03:03:57 +02:00
#include <inttypes.h>
2026-06-15 03:34:03 +02:00
#include <limits.h>
2026-06-15 03:03:57 +02:00
#include <stdlib.h>
#include <string.h>
2026-06-15 02:30:08 +02:00
2026-06-15 02:52:11 +02:00
#include "libavutil/avstring.h"
2026-06-23 16:16:59 +02:00
#include "libavutil/bprint.h"
2026-06-15 02:52:11 +02:00
#include "libavutil/error.h"
#include "libavutil/fifo.h"
2026-06-15 03:34:03 +02:00
#include "libavutil/log.h"
2026-06-15 02:52:11 +02:00
#include "libavutil/macros.h"
#include "libavutil/mem.h"
2026-06-15 02:30:08 +02:00
#include "libavutil/opt.h"
2026-06-15 02:52:11 +02:00
#include "libavutil/thread.h"
#include "libavutil/time.h"
2026-06-15 02:30:08 +02:00
#include "avformat.h"
2026-06-15 02:52:11 +02:00
#include "http.h"
2026-06-15 02:30:08 +02:00
#include "internal.h"
#include "url.h"
2026-06-15 03:34:03 +02:00
#include "version.h"
2026-06-15 02:30:08 +02:00
2026-06-15 03:34:03 +02:00
#define DEFAULT_USER_AGENT "Lavf/" AV_STRINGIFY(LIBAVFORMAT_VERSION)
2026-06-15 02:52:11 +02:00
#define CURL_DEFAULT_BUFFER_SIZE (4 << 20)
2026-06-15 03:34:03 +02:00
2026-06-15 02:52:11 +02:00
/* Blocking waits wake up this often so url_read()/open can poll the interrupt
* callback. */
#define CURL_WAIT_US 100000
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 */
2026-06-15 03:03:57 +02:00
CMD_SEEK , /* restart the transfer at a new byte offset */
2026-06-15 02:52:11 +02:00
};
typedef struct CurlCmd {
enum cmd_kind kind ;
CurlContext * ctx ;
2026-06-15 03:03:57 +02:00
int64_t pos ; /* CMD_SEEK target offset */
int sync ; /* caller waits for completion */
2026-06-15 02:52:11 +02:00
int done ;
struct CurlCmd * next ;
} CurlCmd ;
typedef struct CurlLoop {
2026-06-24 14:12:37 +02:00
AVFormatContext * avfc ; /* owning context (if any) */
2026-06-15 02:52:11 +02:00
pthread_t thread ;
CURLM * multi ;
2026-07-26 13:51:05 +02:00
CURLSH * share ; /* shared cookies/HSTS */
2026-06-15 02:52:11 +02:00
pthread_mutex_t mutex ; /* guards the command queue, exit and cmd->done */
pthread_cond_t cond ; /* signaled when a sync command completes */
CurlCmd * cmd_head , * cmd_tail ;
int exit ;
2026-06-24 14:13:36 +02:00
/* 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_retries ;
2026-06-15 02:52:11 +02:00
} CurlLoop ;
struct CurlContext {
const AVClass * class ;
URLContext * h ;
CurlLoop * loop ;
int private_loop ; /* loop is owned by this context (not shared) */
CURL * easy ;
2026-06-15 03:34:03 +02:00
struct curl_slist * header_list ;
2026-06-15 02:52:11 +02:00
2026-06-15 03:34:03 +02:00
/* 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 ;
2026-06-15 02:52:11 +02:00
int64_t buffer_size ;
2026-06-15 03:34:03 +02:00
int64_t request_size ;
2026-06-24 19:03:04 +02:00
int64_t initial_request_size ;
2026-06-15 03:03:57 +02:00
int max_retries ;
int64_t logical_pos ; /* next byte url_read() will return, caller side */
2026-06-15 02:52:11 +02:00
/* Producer bookkeeping, touched only by the loop thread. */
2026-06-15 03:03:57 +02:00
int active ; /* currently added to the multi */
2026-06-29 16:07:15 +02:00
int64_t request_start ; /* absolute offset the current request began at */
int64_t request_received ; /* bytes delivered in the current request */
2026-06-24 18:44:57 +02:00
int64_t request_end ; /* expected end of request, or -1 if unknown */
2026-06-15 03:03:57 +02:00
int retry_count ; /* consecutive recoverable failures */
2026-06-24 19:03:04 +02:00
int is_initial ; /* using reduced request size */
2026-06-15 03:03:57 +02:00
/* Per-response-block header scratch, loop thread only. */
int hdr_accept_ranges ;
int hdr_compressed ;
2026-06-24 18:35:09 +02:00
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 */
2026-06-15 02:52:11 +02:00
/* Probe result. Set by the loop thread, read by url_open() once probed. */
int probed ;
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 eof ; /* producer delivered all data */
int error ; /* AVERROR for an unrecoverable failure, or 0 */
int aborted ; /* transfer should stop (open was interrupted) */
};
/* 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 : return AVERROR ( EINVAL );
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 );
}
}
2026-06-15 03:03:57 +02:00
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 ;
}
}
2026-06-15 02:52:11 +02:00
/* ------------------------------------------------------------------------- */
/* curl callbacks (run on the loop thread) */
/* ------------------------------------------------------------------------- */
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 ;
}
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 ;
2026-06-15 03:03:57 +02:00
c -> request_received += bytes ;
2026-06-15 02:52:11 +02:00
pthread_cond_broadcast ( & c -> cond );
pthread_mutex_unlock ( & c -> mutex );
return bytes ;
}
2026-06-24 18:35:09 +02:00
static int64_t parse_offset ( const char * s )
2026-06-15 03:03:57 +02:00
{
2026-06-24 18:35:09 +02:00
int64_t v = strtoll ( s , NULL , 10 );
return v < 0 ? - 1 : v ;
}
/* "bytes $from-$to/$document_size" */
static void parse_content_range ( CurlContext * c , const char * v )
{
while ( av_isspace ( * v ))
v ++ ;
if ( av_strncasecmp ( v , "bytes " , 6 ))
return ;
const char * range = v + 6 , * end ;
if ( range [ 0 ] != '*' ) {
c -> hdr_content_start = parse_offset ( range );
if (( end = strchr ( range , '-' )))
c -> hdr_content_end = parse_offset ( end + 1 );
}
const char * slash = strchr ( range , '/' );
if ( slash && slash [ 1 ] != '*' )
c -> hdr_content_total = parse_offset ( slash + 1 );
2026-06-15 03:03:57 +02:00
}
2026-06-15 02:52:11 +02:00
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 ;
2026-06-15 03:03:57 +02:00
if ( av_strncasecmp ( ptr , "HTTP/" , 5 ) == 0 ) {
c -> hdr_accept_ranges = 0 ;
c -> hdr_compressed = 0 ;
2026-06-24 18:35:09 +02:00
c -> hdr_content_start = - 1 ;
c -> hdr_content_end = - 1 ;
2026-06-15 03:03:57 +02:00
c -> hdr_content_total = - 1 ;
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 ) {
2026-06-24 18:35:09 +02:00
parse_content_range ( c , ptr + 14 );
2026-06-15 03:03:57 +02:00
return len ;
}
/* Otherwise act only on the blank line that terminates the header block. */
2026-06-15 02:52:11 +02:00
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 ;
pthread_mutex_lock ( & c -> mutex );
if ( status >= 200 && status < 300 ) {
2026-06-29 17:01:29 +02:00
int64_t content_start = status == 206 ? c -> hdr_content_start : 0 ;
2026-07-26 13:47:27 +02:00
/* The reply must start at the offset we requested: for follow-up
* requests always, for the initial one when an explicit nonzero
* offset was requested. */
if (( c -> probed ? c -> seekable : c -> off > 0 ) &&
content_start != c -> request_start ) {
2026-06-29 17:01:29 +02:00
av_log ( c -> h , AV_LOG_ERROR , "Server sent back unexpected reply "
"with offset %" PRId64 " (expected %" PRId64 ") \n " ,
content_start , c -> request_start );
c -> stream_ok = 0 ;
if ( ! c -> error )
c -> error = AVERROR ( EIO );
pthread_cond_broadcast ( & c -> cond );
pthread_mutex_unlock ( & c -> mutex );
return len ;
}
2026-06-15 02:52:11 +02:00
c -> stream_ok = 1 ;
2026-06-15 03:34:03 +02:00
/* 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 ;
}
}
}
2026-06-15 03:03:57 +02:00
/* A compressed body is addressed in encoded form, so byte offsets are
* meaningless: not seekable. Note that we prefer compression over
2026-07-26 13:51:05 +02:00
* seekability, servers don't offer media in compressed form, so it
2026-06-15 03:03:57 +02:00
* gives us free compression for other payloads like text playlist. */
c -> seekable = ! c -> hdr_compressed &&
( status == 206 || c -> hdr_accept_ranges );
2026-07-26 13:48:09 +02:00
if ( ! c -> hdr_compressed ) {
2026-06-15 03:03:57 +02:00
int64_t total = c -> hdr_content_total ;
if ( total < 0 && status != 206 ) {
curl_off_t cl = - 1 ;
if ( curl_easy_getinfo ( c -> easy , CURLINFO_CONTENT_LENGTH_DOWNLOAD_T ,
& cl ) == CURLE_OK && cl >= 0 )
total = cl ;
}
2026-07-26 13:48:09 +02:00
/* Don't unlearn a known size when a reply omits it. */
if ( total >= 0 )
c -> content_size = total ;
}
if ( c -> seekable ) {
2026-06-24 18:44:57 +02:00
if ( c -> hdr_content_end >= 0 )
c -> request_end = c -> hdr_content_end ;
else
2026-06-29 16:33:37 +02:00
c -> request_end = c -> content_size > 0 ? c -> content_size - 1 : - 1 ;
2026-06-15 03:03:57 +02:00
}
2026-07-26 13:48:35 +02:00
/* 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 ;
2026-06-15 02:52:11 +02:00
} else {
c -> stream_ok = 0 ;
if ( ! c -> error )
c -> error = ff_http_averror ( status , AVERROR ( EIO ));
}
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 */
}
2026-06-15 03:03:57 +02:00
/* (Re)issue the request for the current offset and add it to the multi. Loop
* thread only. */
static void start_request ( CurlContext * c )
{
if ( ! c -> probed || c -> seekable ) {
2026-06-29 16:07:15 +02:00
int64_t start = c -> request_start ;
2026-06-15 03:34:03 +02:00
char range [ 48 ];
2026-06-24 19:03:04 +02:00
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 ) {
2026-06-29 16:07:15 +02:00
int64_t end = INT64_MAX ;
2026-06-29 16:57:34 +02:00
if ( request_size > 0 && start <= INT64_MAX - request_size )
2026-06-24 19:03:04 +02:00
end = start + request_size - 1 ;
2026-06-15 03:34:03 +02:00
if ( c -> content_size > 0 )
2026-06-29 16:07:15 +02:00
end = FFMIN ( end , c -> content_size - 1 );
2026-06-15 03:34:03 +02:00
if ( c -> end_off > 0 )
2026-06-29 16:07:15 +02:00
end = FFMIN ( end , c -> end_off - 1 );
snprintf ( range , sizeof ( range ), "%" PRId64 "-%" PRId64 , start , end );
2026-06-15 03:34:03 +02:00
} else {
2026-06-29 16:07:15 +02:00
snprintf ( range , sizeof ( range ), "%" PRId64 "-" , start );
2026-06-15 03:34:03 +02:00
}
2026-06-15 03:03:57 +02:00
curl_easy_setopt ( c -> easy , CURLOPT_RANGE , range );
} else {
curl_easy_setopt ( c -> easy , CURLOPT_RANGE , NULL );
}
2026-06-24 14:13:36 +02:00
c -> loop -> num_requests ++ ;
2026-06-15 03:03:57 +02:00
c -> request_received = 0 ;
2026-06-24 18:44:57 +02:00
c -> request_end = - 1 ;
2026-06-15 03:03:57 +02:00
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 );
if ( ! c -> error )
c -> error = AVERROR ( EIO );
pthread_cond_broadcast ( & c -> cond );
pthread_mutex_unlock ( & c -> mutex );
}
}
2026-06-24 14:13:36 +02:00
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 ;
}
2026-06-15 02:52:11 +02:00
/* Transfer finished (or failed) */
static void on_done ( CurlContext * c , CURLcode code )
{
2026-06-29 16:07:15 +02:00
int64_t received ;
2026-06-15 03:03:57 +02:00
int aborted ;
2026-06-15 02:52:11 +02:00
pthread_mutex_lock ( & c -> mutex );
2026-06-15 03:34:03 +02:00
aborted = c -> aborted ;
received = c -> request_received ;
2026-06-15 03:03:57 +02:00
/* Advance past delivered bytes so a retry or seek resumes at the right offset. */
2026-06-29 16:58:18 +02:00
if ( received > INT64_MAX - c -> request_start ) {
if ( ! c -> error )
c -> error = AVERROR ( EIO );
received = 0 ;
aborted = 1 ;
pthread_cond_broadcast ( & c -> cond );
}
2026-06-15 03:34:03 +02:00
c -> request_start += received ;
2026-06-15 03:03:57 +02:00
c -> request_received = 0 ;
pthread_mutex_unlock ( & c -> mutex );
2026-06-24 14:13:36 +02:00
update_statistics ( c );
2026-06-15 03:03:57 +02:00
2026-06-15 02:52:11 +02:00
if ( ! c -> probed ) {
/* Connection died before any usable header arrived. */
2026-06-15 03:03:57 +02:00
pthread_mutex_lock ( & c -> mutex );
c -> probed = 1 ;
2026-06-15 02:52:11 +02:00
c -> stream_ok = 0 ;
if ( ! c -> error )
c -> error = curlcode_to_averror ( code );
2026-06-15 03:03:57 +02:00
pthread_cond_broadcast ( & c -> cond );
pthread_mutex_unlock ( & c -> mutex );
return ;
}
if ( code == CURLE_OK && ! aborted && c -> stream_ok ) {
2026-06-15 03:34:03 +02:00
c -> retry_count = 0 ;
2026-06-29 16:33:37 +02:00
int64_t file_end = c -> content_size > 0 ? c -> content_size - 1 : - 1 ;
2026-06-24 18:44:57 +02:00
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 ) {
2026-06-24 19:03:04 +02:00
c -> is_initial = 0 ;
2026-06-24 18:44:57 +02:00
start_request ( c );
return ;
2026-06-15 03:34:03 +02:00
}
2026-06-15 03:03:57 +02:00
pthread_mutex_lock ( & c -> mutex );
2026-06-15 02:52:11 +02:00
c -> eof = 1 ;
2026-06-15 03:03:57 +02:00
pthread_cond_broadcast ( & c -> cond );
pthread_mutex_unlock ( & c -> mutex );
return ;
}
/* Resume seekable transfers after a recoverable error. */
if ( ! aborted && c -> seekable && is_recoverable ( code ) &&
c -> retry_count < c -> max_retries ) {
c -> retry_count ++ ;
2026-06-24 14:13:36 +02:00
c -> loop -> num_retries ++ ;
2026-06-29 16:07:15 +02:00
av_log ( c -> h , AV_LOG_WARNING , "%s, retrying (#%d) from %" PRId64 " \n " ,
2026-06-15 03:03:57 +02:00
curl_easy_strerror ( code ), c -> retry_count , c -> request_start );
start_request ( c );
return ;
}
if ( ! aborted ) {
pthread_mutex_lock ( & c -> mutex );
if ( ! c -> error )
c -> error = curlcode_to_averror ( code );
pthread_cond_broadcast ( & c -> cond );
pthread_mutex_unlock ( & c -> mutex );
2026-06-15 02:52:11 +02:00
}
}
/* ------------------------------------------------------------------------- */
/* event loop thread + command queue */
/* ------------------------------------------------------------------------- */
static void execute_command ( CurlLoop * loop , CurlCmd * cmd )
{
CurlContext * c = cmd -> ctx ;
switch ( cmd -> kind ) {
case CMD_ADD :
2026-06-15 03:03:57 +02:00
start_request ( c );
2026-06-15 02:52:11 +02:00
break ;
case CMD_REMOVE :
if ( c -> active ) {
curl_multi_remove_handle ( loop -> multi , c -> easy );
2026-06-24 14:13:36 +02:00
update_statistics ( c );
2026-06-15 02:52:11 +02:00
c -> active = 0 ;
}
break ;
case CMD_UNPAUSE :
2026-07-26 13:49:43 +02:00
pthread_mutex_lock ( & c -> mutex );
c -> paused = 0 ;
pthread_mutex_unlock ( & c -> mutex );
2026-06-15 02:52:11 +02:00
curl_easy_pause ( c -> easy , CURLPAUSE_CONT );
break ;
2026-06-15 03:03:57 +02:00
case CMD_SEEK :
if ( c -> active ) {
curl_multi_remove_handle ( loop -> multi , c -> easy );
c -> active = 0 ;
}
pthread_mutex_lock ( & c -> mutex );
av_fifo_reset2 ( c -> fifo );
c -> paused = 0 ;
c -> eof = 0 ;
c -> error = 0 ;
pthread_mutex_unlock ( & c -> mutex );
c -> request_start = cmd -> pos ;
c -> retry_count = 0 ;
start_request ( c );
break ;
2026-06-15 02:52:11 +02:00
}
}
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. */
2026-06-15 03:03:57 +02:00
static int curl_dispatch ( CurlLoop * loop , enum cmd_kind kind , CurlContext * c ,
int64_t pos , int sync )
2026-06-15 02:52:11 +02:00
{
CurlCmd stackcmd = { 0 };
CurlCmd * cmd = sync ? & stackcmd : av_mallocz ( sizeof ( * cmd ));
if ( ! cmd )
return AVERROR ( ENOMEM );
cmd -> kind = kind ;
cmd -> ctx = c ;
2026-06-15 03:03:57 +02:00
cmd -> pos = pos ;
2026-06-15 02:52:11 +02:00
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 ;
}
2026-06-24 14:12:37 +02:00
static CurlLoop * curl_loop_create ( AVFormatContext * avfc )
2026-06-15 02:52:11 +02:00
{
CurlLoop * loop = av_mallocz ( sizeof ( * loop ));
if ( ! loop )
return NULL ;
2026-06-24 14:12:37 +02:00
loop -> avfc = avfc ;
2026-06-15 02:52:11 +02:00
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 ;
curl_multi_setopt ( loop -> multi , CURLMOPT_PIPELINING , CURLPIPE_MULTIPLEX );
2026-06-23 15:55:28 +02:00
loop -> share = curl_share_init ();
if ( ! loop -> share )
goto fail3 ;
curl_share_setopt ( loop -> share , CURLSHOPT_SHARE , CURL_LOCK_DATA_COOKIE );
curl_share_setopt ( loop -> share , CURLSHOPT_SHARE , CURL_LOCK_DATA_HSTS );
if ( pthread_create ( & loop -> thread , NULL , curl_worker , loop ))
2026-06-15 02:52:11 +02:00
goto fail3 ;
return loop ;
fail3 :
2026-06-23 15:55:28 +02:00
curl_multi_cleanup ( loop -> multi );
curl_share_cleanup ( loop -> share );
2026-06-15 02:52:11 +02:00
curl_global_cleanup ();
fail2 :
pthread_cond_destroy ( & loop -> cond );
pthread_mutex_destroy ( & loop -> mutex );
fail :
av_free ( loop );
return NULL ;
}
2026-06-24 14:13:36 +02:00
static void print_statistics ( CurlLoop * loop )
{
AVFormatContext * avfc = loop -> avfc ;
if ( ! loop -> total_bytes )
return ;
double time = ( double ) 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 );
av_log ( avfc , AV_LOG_VERBOSE ,
"libcurl: %d connections, %d redirects, %d requests, %d retries \n " ,
loop -> num_connections , loop -> num_redirects , loop -> num_requests , loop -> num_retries );
}
2026-06-15 02:52:11 +02:00
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 );
2026-06-24 14:13:36 +02:00
print_statistics ( loop );
2026-06-15 02:52:11 +02:00
curl_multi_cleanup ( loop -> multi );
2026-06-23 15:55:28 +02:00
curl_share_cleanup ( loop -> share );
2026-06-15 02:52:11 +02:00
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 ) {
2026-06-24 14:12:37 +02:00
c -> loop = curl_loop_create ( NULL );
2026-06-15 02:52:11 +02:00
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 ) {
2026-06-24 14:12:37 +02:00
c -> loop = curl_loop_create ( avfc );
2026-06-15 02:52:11 +02:00
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 );
2026-06-15 03:34:03 +02:00
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 ;
}
2026-06-23 16:16:59 +02:00
static int setup_protocols ( CurlContext * c )
{
const char * wl = c -> h -> protocol_whitelist ;
const char * bl = c -> h -> protocol_blacklist ;
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 );
}
2026-07-26 13:50:40 +02:00
if ( ! av_bprint_is_complete ( & bp )) {
av_bprint_finalize ( & bp , NULL );
2026-06-23 16:16:59 +02:00
return AVERROR ( ENOMEM );
2026-07-26 13:50:40 +02:00
}
2026-06-23 16:16:59 +02:00
if ( ! bp . len ) {
av_log ( c -> h , AV_LOG_ERROR , "Set of allowed protocols is empty. \n " );
2026-07-26 13:50:40 +02:00
av_bprint_finalize ( & bp , NULL );
2026-06-23 16:16:59 +02:00
return AVERROR ( EINVAL );
}
curl_easy_setopt ( c -> easy , CURLOPT_PROTOCOLS_STR , bp . str );
curl_easy_setopt ( c -> easy , CURLOPT_REDIR_PROTOCOLS_STR , bp . str );
av_bprint_finalize ( & bp , NULL );
return 0 ;
}
2026-06-15 02:52:11 +02:00
static void setup_curl ( CurlContext * c )
{
CURL * e = c -> easy ;
const char * url = c -> h -> filename ;
/* Drop an optional "libcurl:" prefix that forces this protocol. */
av_strstart ( url , "libcurl:" , & url );
curl_easy_setopt ( e , CURLOPT_URL , url );
curl_easy_setopt ( e , CURLOPT_PRIVATE , c );
curl_easy_setopt ( e , CURLOPT_NOSIGNAL , 1L );
2026-06-23 15:55:28 +02:00
curl_easy_setopt ( e , CURLOPT_SHARE , c -> loop -> share );
2026-06-15 02:52:11 +02:00
curl_easy_setopt ( e , CURLOPT_WRITEFUNCTION , write_callback );
curl_easy_setopt ( e , CURLOPT_WRITEDATA , c );
curl_easy_setopt ( e , CURLOPT_HEADERFUNCTION , header_callback );
curl_easy_setopt ( e , CURLOPT_HEADERDATA , c );
curl_easy_setopt ( e , CURLOPT_NOPROGRESS , 0L );
curl_easy_setopt ( e , CURLOPT_XFERINFOFUNCTION , xferinfo_callback );
curl_easy_setopt ( e , CURLOPT_XFERINFODATA , c );
2026-06-15 03:34:03 +02:00
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 );
}
2026-06-15 02:52:11 +02:00
curl_easy_setopt ( e , CURLOPT_FOLLOWLOCATION , 1L );
2026-06-15 03:34:03 +02:00
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 );
2026-07-26 13:47:47 +02:00
curl_easy_setopt ( e , CURLOPT_ACCEPT_ENCODING ,
c -> off > 0 || c -> end_off > 0 ? "identity" : "" );
2026-06-15 03:34:03 +02:00
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 );
2026-06-23 15:56:53 +02:00
curl_easy_setopt ( e , CURLOPT_COOKIEFILE , "" );
2026-06-15 03:34:03 +02:00
if ( c -> cookies && c -> cookies [ 0 ]) {
char * copy = av_strdup ( c -> cookies );
char * line , * saveptr = NULL ;
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 );
2026-06-15 02:52:11 +02:00
}
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 -> error ) {
if ( ff_check_interrupt ( & h -> interrupt_callback )) {
c -> aborted = 1 ;
ret = AVERROR_EXIT ;
break ;
}
curl_cond_wait ( c );
}
if ( ! ret ) {
if ( ! c -> stream_ok )
ret = c -> error ? c -> error : AVERROR ( EIO );
}
pthread_mutex_unlock ( & c -> mutex );
return ret ;
}
2026-06-15 02:30:08 +02:00
static int libcurl_open ( URLContext * h , const char * url , int flags ,
AVDictionary ** options )
{
2026-06-15 02:52:11 +02:00
/* 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 ;
2026-06-15 03:34:03 +02:00
const char * eff_url = h -> filename ;
2026-06-15 02:52:11 +02:00
int ret ;
c -> h = h ;
c -> content_size = - 1 ;
2026-06-15 03:34:03 +02:00
c -> request_start = c -> off ;
2026-06-24 18:44:57 +02:00
c -> request_end = - 1 ;
2026-06-15 03:34:03 +02:00
c -> logical_pos = c -> off ;
2026-06-24 19:03:04 +02:00
c -> is_initial = 1 ;
2026-06-15 03:34:03 +02:00
/* 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 );
2026-06-15 02:52:11 +02:00
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 ;
}
2026-06-23 16:16:59 +02:00
ret = setup_protocols ( c );
if ( ret < 0 )
goto fail ;
2026-06-15 02:52:11 +02:00
setup_curl ( c );
2026-06-15 03:03:57 +02:00
ret = curl_dispatch ( c -> loop , CMD_ADD , c , 0 , 0 );
2026-06-15 02:52:11 +02:00
if ( ret < 0 )
goto fail ;
ret = wait_for_probe ( c );
if ( ret < 0 )
goto fail ;
2026-07-26 13:48:35 +02:00
pthread_mutex_lock ( & c -> mutex );
2026-06-15 02:52:11 +02:00
h -> is_streamed = ! c -> seekable ;
2026-07-26 13:48:35 +02:00
pthread_mutex_unlock ( & c -> mutex );
2026-06-15 02:52:11 +02:00
return 0 ;
fail :
libcurl_close ( h );
return ret ;
2026-06-15 02:30:08 +02:00
}
static int libcurl_read ( URLContext * h , unsigned char * buf , int size )
{
2026-06-15 02:52:11 +02:00
CurlContext * c = h -> priv_data ;
int nonblock = h -> flags & AVIO_FLAG_NONBLOCK ;
int ret ;
pthread_mutex_lock ( & c -> mutex );
while ( 1 ) {
size_t avail = av_fifo_can_read ( c -> fifo );
if ( avail ) {
int n = FFMIN ( avail , ( size_t ) size );
int unpause ;
av_fifo_read ( c -> fifo , buf , n );
/* Resume a paused transfer once the FIFO is at least half empty. */
unpause = c -> paused && av_fifo_can_write ( c -> fifo ) * 2 >= c -> buffer_size ;
2026-06-15 03:03:57 +02:00
c -> logical_pos += n ;
2026-06-15 02:52:11 +02:00
pthread_mutex_unlock ( & c -> mutex );
if ( unpause )
2026-06-15 03:03:57 +02:00
curl_dispatch ( c -> loop , CMD_UNPAUSE , c , 0 , 0 );
2026-06-15 02:52:11 +02:00
return n ;
}
if ( c -> error ) {
ret = c -> error ;
break ;
}
if ( c -> eof ) {
ret = AVERROR_EOF ;
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 ;
}
pthread_mutex_unlock ( & c -> mutex );
return ret ;
2026-06-15 02:30:08 +02:00
}
2026-06-15 03:03:57 +02:00
static int64_t libcurl_seek ( URLContext * h , int64_t pos , int whence )
{
CurlContext * c = h -> priv_data ;
int64_t newpos ;
2026-06-23 15:43:11 +02:00
pthread_mutex_lock ( & c -> mutex );
const int64_t content_size = c -> content_size ;
const int seekable = c -> seekable ;
pthread_mutex_unlock ( & c -> mutex );
2026-06-15 03:03:57 +02:00
2026-06-23 15:43:11 +02:00
if ( whence == AVSEEK_SIZE )
return content_size >= 0 ? content_size : AVERROR ( ENOSYS );
if ( ! seekable )
2026-06-15 03:03:57 +02:00
return AVERROR ( ENOSYS );
switch ( whence ) {
case SEEK_SET :
newpos = pos ;
break ;
case SEEK_CUR :
2026-06-29 16:57:08 +02:00
if ( pos > INT64_MAX - c -> logical_pos )
return AVERROR ( ERANGE );
2026-06-15 03:03:57 +02:00
newpos = c -> logical_pos + pos ;
break ;
case SEEK_END :
2026-06-23 15:43:11 +02:00
if ( content_size < 0 )
2026-06-15 03:03:57 +02:00
return AVERROR ( ENOSYS );
2026-06-29 16:57:08 +02:00
if ( pos > INT64_MAX - content_size )
return AVERROR ( ERANGE );
2026-06-23 15:43:11 +02:00
newpos = content_size + pos ;
2026-06-15 03:03:57 +02:00
break ;
default :
return AVERROR ( EINVAL );
}
if ( newpos < 0 )
return AVERROR ( EINVAL );
2026-06-24 17:39:40 +02:00
if ( newpos == c -> logical_pos )
return newpos ;
2026-06-15 03:03:57 +02:00
/* 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 ;
return newpos ;
}
2026-06-15 02:30:08 +02:00
static int libcurl_close ( URLContext * h )
{
2026-06-15 02:52:11 +02:00
CurlContext * c = h -> priv_data ;
if ( c -> loop ) {
if ( c -> easy ) {
/* Ensure the handle is out of the multi before we free it. */
2026-06-15 03:03:57 +02:00
curl_dispatch ( c -> loop , CMD_REMOVE , c , 0 , 1 );
2026-06-15 02:52:11 +02:00
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 ;
}
2026-06-15 03:34:03 +02:00
if ( c -> header_list )
curl_slist_free_all ( c -> header_list );
2026-06-15 02:52:11 +02:00
av_fifo_freep2 ( & c -> fifo );
pthread_cond_destroy ( & c -> cond );
pthread_mutex_destroy ( & c -> mutex );
2026-06-15 02:30:08 +02:00
return 0 ;
}
2026-06-15 03:34:03 +02:00
#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 },
2026-06-29 15:45:18 +02:00
{ "connect_timeout" , "connection timeout in seconds (0 = libcurl default)" , OFFSET ( connect_timeout ), AV_OPT_TYPE_INT , { . i64 = 0 }, 0 , INT_MAX / 1000 , D | E },
2026-06-15 03:34:03 +02:00
{ "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 },
2026-07-26 13:50:51 +02:00
{ "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 },
2026-06-15 03:34:03 +02:00
{ "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 },
2026-06-24 19:03:04 +02:00
{ "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 },
2026-06-15 03:34:03 +02:00
{ "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 }
};
2026-06-15 02:30:08 +02:00
static const AVClass libcurl_context_class = {
. class_name = "libcurl" ,
. item_name = av_default_item_name ,
2026-06-15 03:34:03 +02:00
. option = options ,
2026-06-15 02:30:08 +02:00
. version = LIBAVUTIL_VERSION_INT ,
};
const URLProtocol ff_libcurl_protocol = {
. name = "libcurl" ,
. url_open2 = libcurl_open ,
. url_read = libcurl_read ,
2026-06-15 03:03:57 +02:00
. url_seek = libcurl_seek ,
2026-06-15 02:30:08 +02:00
. url_close = libcurl_close ,
. priv_data_size = sizeof ( CurlContext ),
. priv_data_class = & libcurl_context_class ,
. flags = URL_PROTOCOL_FLAG_NETWORK ,
2026-06-23 16:16:59 +02:00
. default_whitelist = "http,https,libcurl" ,
2026-06-15 02:30:08 +02:00
};