FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/libcurl.c
Date: 2026-09-01 23:28:12
Exec Total Coverage
Lines: 3 665 0.5%
Functions: 1 28 3.6%
Branches: 2 394 0.5%

Line Branch Exec Source
1 /*
2 * libcurl based HTTP(S) protocol
3 * Copyright (C) 2026 Kacper Michajłow
4 *
5 * This file is part of FFmpeg.
6 *
7 * FFmpeg is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
11 *
12 * FFmpeg is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with FFmpeg; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 */
21
22 #include "config_components.h"
23
24 #include <curl/curl.h>
25 #include <inttypes.h>
26 #include <limits.h>
27 #include <stdlib.h>
28 #include <string.h>
29
30 #include "libavutil/avstring.h"
31 #include "libavutil/bprint.h"
32 #include "libavutil/error.h"
33 #include "libavutil/fifo.h"
34 #include "libavutil/log.h"
35 #include "libavutil/macros.h"
36 #include "libavutil/mem.h"
37 #include "libavutil/opt.h"
38 #include "libavutil/thread.h"
39 #include "libavutil/time.h"
40
41 #include "avformat.h"
42 #include "http.h"
43 #include "internal.h"
44 #include "url.h"
45 #include "version.h"
46
47 #define DEFAULT_USER_AGENT "Lavf/" AV_STRINGIFY(LIBAVFORMAT_VERSION)
48 #define CURL_DEFAULT_BUFFER_SIZE (4 << 20)
49
50 /* Blocking waits wake up this often so url_read()/open can poll the interrupt
51 * callback. */
52 #define CURL_WAIT_US 100000
53
54 typedef struct CurlContext CurlContext;
55
56 enum cmd_kind {
57 CMD_ADD, /* add the easy handle to the multi and start the transfer */
58 CMD_REMOVE, /* remove the easy handle from the multi */
59 CMD_UNPAUSE, /* resume a transfer paused because the FIFO was full */
60 CMD_SEEK, /* restart the transfer at a new byte offset */
61 };
62
63 typedef struct CurlCmd {
64 enum cmd_kind kind;
65 CurlContext *ctx;
66 int64_t pos; /* CMD_SEEK target offset */
67 int sync; /* caller waits for completion */
68 int done;
69 struct CurlCmd *next;
70 } CurlCmd;
71
72 typedef struct CurlLoop {
73 AVFormatContext *avfc; /* owning context (if any) */
74
75 pthread_t thread;
76 CURLM *multi;
77 CURLSH *share; /* shared cookies/HSTS */
78
79 pthread_mutex_t mutex; /* guards the command queue, exit and cmd->done */
80 pthread_cond_t cond; /* signaled when a sync command completes */
81 CurlCmd *cmd_head, *cmd_tail;
82 int exit;
83
84 /* Connection statistics (updated by loop thread) */
85 int64_t total_bytes;
86 int64_t total_time_us;
87 int num_connections;
88 int num_redirects;
89 int num_requests;
90 int num_retries;
91 } CurlLoop;
92
93 struct CurlContext {
94 const AVClass *class;
95 URLContext *h;
96
97 CurlLoop *loop;
98 int private_loop; /* loop is owned by this context (not shared) */
99 CURL *easy;
100 struct curl_slist *header_list;
101
102 /* AVOptions. */
103 char *user_agent;
104 char *referer;
105 char *headers;
106 char *http_proxy;
107 char *cookies;
108 char *ca_file;
109 char *cert_file;
110 char *key_file;
111 char *location; /* effective URL after redirects (output) */
112 int64_t off; /* initial byte offset */
113 int64_t end_off; /* exclusive upper byte bound (0 = none) */
114 int tls_verify;
115 int seekable_opt;
116 int connect_timeout;
117 int max_redirects;
118 int multiple_requests;
119 int http_version;
120 int64_t buffer_size;
121 int64_t request_size;
122 int64_t initial_request_size;
123 int max_retries;
124
125 int64_t logical_pos; /* next byte url_read() will return, caller side */
126
127 /* Producer bookkeeping, touched only by the loop thread. */
128 int active; /* currently added to the multi */
129 int64_t request_start; /* absolute offset the current request began at */
130 int64_t request_received;/* bytes delivered in the current request */
131 int64_t request_end; /* expected end of request, or -1 if unknown */
132 int retry_count; /* consecutive recoverable failures */
133 int is_initial; /* using reduced request size */
134
135 /* Per-response-block header scratch, loop thread only. */
136 int hdr_accept_ranges;
137 int hdr_compressed;
138 int64_t hdr_content_start; /* inclusive start, or -1 */
139 int64_t hdr_content_end; /* inclusive end, or -1 */
140 int64_t hdr_content_total; /* if known, or -1 */
141
142 /* Probe result. Set by the loop thread, read by url_open() once probed. */
143 int probed;
144 int stream_ok;
145 int seekable;
146 int64_t content_size;
147
148 /* Shared transfer state, guarded by mutex. */
149 pthread_mutex_t mutex;
150 pthread_cond_t cond;
151 AVFifo *fifo;
152 int paused; /* write callback paused, FIFO was full */
153 int eof; /* producer delivered all data */
154 int error; /* AVERROR for an unrecoverable failure, or 0 */
155 int aborted; /* transfer should stop (open was interrupted) */
156 };
157
158 /* Guards lazy creation of a format context's shared loop. */
159 static AVMutex curl_loop_lock = AV_MUTEX_INITIALIZER;
160
161 static int curlcode_to_averror(CURLcode code)
162 {
163 switch (code) {
164 case CURLE_OK: return 0;
165 case CURLE_URL_MALFORMAT:
166 case CURLE_UNSUPPORTED_PROTOCOL: return AVERROR(EINVAL);
167 case CURLE_COULDNT_RESOLVE_PROXY:
168 case CURLE_COULDNT_RESOLVE_HOST: return AVERROR(EHOSTUNREACH);
169 case CURLE_COULDNT_CONNECT: return AVERROR(ECONNREFUSED);
170 case CURLE_OPERATION_TIMEDOUT: return AVERROR(ETIMEDOUT);
171 case CURLE_LOGIN_DENIED:
172 case CURLE_REMOTE_ACCESS_DENIED: return AVERROR(EACCES);
173 case CURLE_OUT_OF_MEMORY: return AVERROR(ENOMEM);
174 case CURLE_PEER_FAILED_VERIFICATION:
175 case CURLE_SSL_CACERT_BADFILE: return AVERROR_INVALIDDATA;
176 default: return AVERROR(EIO);
177 }
178 }
179
180 static int is_recoverable(CURLcode code)
181 {
182 switch (code) {
183 case CURLE_RECV_ERROR:
184 case CURLE_SEND_ERROR:
185 case CURLE_PARTIAL_FILE:
186 case CURLE_OPERATION_TIMEDOUT:
187 case CURLE_GOT_NOTHING:
188 case CURLE_COULDNT_CONNECT:
189 case CURLE_COULDNT_RESOLVE_HOST:
190 case CURLE_HTTP2:
191 case CURLE_HTTP2_STREAM:
192 return 1;
193 default:
194 return 0;
195 }
196 }
197
198 /* ------------------------------------------------------------------------- */
199 /* curl callbacks (run on the loop thread) */
200 /* ------------------------------------------------------------------------- */
201
202 static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata)
203 {
204 CurlContext *c = userdata;
205 size_t bytes = size * nmemb;
206 size_t space;
207
208 pthread_mutex_lock(&c->mutex);
209
210 if (c->aborted || !c->stream_ok) {
211 pthread_mutex_unlock(&c->mutex);
212 return CURL_WRITEFUNC_ERROR;
213 }
214
215 space = av_fifo_can_write(c->fifo);
216 if (space < bytes) {
217 /* pause the transfer and wait for the consumer to drain. */
218 c->paused = 1;
219 pthread_mutex_unlock(&c->mutex);
220 return CURL_WRITEFUNC_PAUSE;
221 }
222
223 av_fifo_write(c->fifo, ptr, bytes);
224 c->paused = 0;
225 c->request_received += bytes;
226 pthread_cond_broadcast(&c->cond);
227 pthread_mutex_unlock(&c->mutex);
228
229 return bytes;
230 }
231
232 static int64_t parse_offset(const char *s)
233 {
234 int64_t v = strtoll(s, NULL, 10);
235 return v < 0 ? -1 : v;
236 }
237
238 /* "bytes $from-$to/$document_size" */
239 static void parse_content_range(CurlContext *c, const char *v)
240 {
241 while (av_isspace(*v))
242 v++;
243
244 if (av_strncasecmp(v, "bytes ", 6))
245 return;
246
247 const char *range = v + 6, *end;
248 if (range[0] != '*') {
249 c->hdr_content_start = parse_offset(range);
250 if ((end = strchr(range, '-')))
251 c->hdr_content_end = parse_offset(end + 1);
252 }
253
254 const char *slash = strchr(range, '/');
255 if (slash && slash[1] != '*')
256 c->hdr_content_total = parse_offset(slash + 1);
257 }
258
259 static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userdata)
260 {
261 CurlContext *c = userdata;
262 size_t len = size * nitems;
263 size_t n = len;
264 long status = 0;
265
266 if (av_strncasecmp(ptr, "HTTP/", 5) == 0) {
267 c->hdr_accept_ranges = 0;
268 c->hdr_compressed = 0;
269 c->hdr_content_start = -1;
270 c->hdr_content_end = -1;
271 c->hdr_content_total = -1;
272 return len;
273 }
274 if (av_strncasecmp(ptr, "Accept-Ranges:", 14) == 0) {
275 c->hdr_accept_ranges = !!av_stristr(ptr + 14, "bytes");
276 return len;
277 }
278 if (av_strncasecmp(ptr, "Content-Encoding:", 17) == 0) {
279 c->hdr_compressed = !av_stristr(ptr + 17, "identity");
280 return len;
281 }
282 if (av_strncasecmp(ptr, "Content-Range:", 14) == 0) {
283 parse_content_range(c, ptr + 14);
284 return len;
285 }
286
287 /* Otherwise act only on the blank line that terminates the header block. */
288 while (n && (ptr[n - 1] == '\r' || ptr[n - 1] == '\n'))
289 n--;
290 if (n)
291 return len;
292
293 curl_easy_getinfo(c->easy, CURLINFO_RESPONSE_CODE, &status);
294
295 /* Interim (1xx) and redirect (3xx) responses produce an intermediate header
296 * block, wait for the final one. */
297 if (status < 200 || (status >= 300 && status < 400))
298 return len;
299
300 pthread_mutex_lock(&c->mutex);
301 if (status >= 200 && status < 300) {
302 int64_t content_start = status == 206 ? c->hdr_content_start : 0;
303 /* The reply must start at the offset we requested: for follow-up
304 * requests always, for the initial one when an explicit nonzero
305 * offset was requested. */
306 if ((c->probed ? c->seekable : c->off > 0) &&
307 content_start != c->request_start) {
308 av_log(c->h, AV_LOG_ERROR, "Server sent back unexpected reply "
309 "with offset %"PRId64" (expected %"PRId64")\n",
310 content_start, c->request_start);
311 c->stream_ok = 0;
312 if (!c->error)
313 c->error = AVERROR(EIO);
314 pthread_cond_broadcast(&c->cond);
315 pthread_mutex_unlock(&c->mutex);
316 return len;
317 }
318
319 c->stream_ok = 1;
320 /* Capture the post-redirect URL, this is exposed as "location" AVOption
321 * for compatibility with http.c. */
322 if (!c->probed) {
323 const char *eff = NULL;
324 if (curl_easy_getinfo(c->easy, CURLINFO_EFFECTIVE_URL, &eff) == CURLE_OK
325 && eff) {
326 char *dup = av_strdup(eff);
327 if (dup) {
328 av_free(c->location);
329 c->location = dup;
330 }
331 }
332 }
333 /* A compressed body is addressed in encoded form, so byte offsets are
334 * meaningless: not seekable. Note that we prefer compression over
335 * seekability, servers don't offer media in compressed form, so it
336 * gives us free compression for other payloads like text playlist. */
337 c->seekable = !c->hdr_compressed &&
338 (status == 206 || c->hdr_accept_ranges);
339 if (!c->hdr_compressed) {
340 int64_t total = c->hdr_content_total;
341 if (total < 0 && status != 206) {
342 curl_off_t cl = -1;
343 if (curl_easy_getinfo(c->easy, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T,
344 &cl) == CURLE_OK && cl >= 0)
345 total = cl;
346 }
347 /* Don't unlearn a known size when a reply omits it. */
348 if (total >= 0)
349 c->content_size = total;
350 }
351 if (c->seekable) {
352 if (c->hdr_content_end >= 0)
353 c->request_end = c->hdr_content_end;
354 else
355 c->request_end = c->content_size > 0 ? c->content_size - 1 : -1;
356 }
357 /* Apply the user override on every reply so re-evaluation of a
358 * follow-up reply doesn't clobber it. */
359 if (c->seekable_opt >= 0)
360 c->seekable = c->seekable_opt;
361 } else {
362 c->stream_ok = 0;
363 if (!c->error)
364 c->error = ff_http_averror(status, AVERROR(EIO));
365 }
366 c->probed = 1;
367 pthread_cond_broadcast(&c->cond);
368 pthread_mutex_unlock(&c->mutex);
369
370 return len;
371 }
372
373 static int xferinfo_callback(void *userdata, curl_off_t dltotal, curl_off_t dlnow,
374 curl_off_t ultotal, curl_off_t ulnow)
375 {
376 CurlContext *c = userdata;
377 int aborted;
378 pthread_mutex_lock(&c->mutex);
379 aborted = c->aborted;
380 pthread_mutex_unlock(&c->mutex);
381 return aborted; /* non-zero aborts the transfer */
382 }
383
384 /* (Re)issue the request for the current offset and add it to the multi. Loop
385 * thread only. */
386 static void start_request(CurlContext *c)
387 {
388 if (!c->probed || c->seekable) {
389 int64_t start = c->request_start;
390 char range[48];
391 int64_t request_size = c->request_size;
392 if (c->is_initial && c->initial_request_size > 0)
393 request_size = c->initial_request_size;
394 if (request_size > 0 || c->end_off > 0) {
395 int64_t end = INT64_MAX;
396 if (request_size > 0 && start <= INT64_MAX - request_size)
397 end = start + request_size - 1;
398 if (c->content_size > 0)
399 end = FFMIN(end, c->content_size - 1);
400 if (c->end_off > 0)
401 end = FFMIN(end, c->end_off - 1);
402 snprintf(range, sizeof(range), "%"PRId64"-%"PRId64, start, end);
403 } else {
404 snprintf(range, sizeof(range), "%"PRId64"-", start);
405 }
406 curl_easy_setopt(c->easy, CURLOPT_RANGE, range);
407 } else {
408 curl_easy_setopt(c->easy, CURLOPT_RANGE, NULL);
409 }
410 c->loop->num_requests++;
411 c->request_received = 0;
412 c->request_end = -1;
413 c->active = 1;
414 CURLMcode res = curl_multi_add_handle(c->loop->multi, c->easy);
415 if (res != CURLM_OK) {
416 av_log(c->h, AV_LOG_ERROR, "curl_multi_add_handle: %s\n",
417 curl_multi_strerror(res));
418 c->active = 0;
419 pthread_mutex_lock(&c->mutex);
420 if (!c->error)
421 c->error = AVERROR(EIO);
422 pthread_cond_broadcast(&c->cond);
423 pthread_mutex_unlock(&c->mutex);
424 }
425 }
426
427 static void update_statistics(CurlContext *c)
428 {
429 CurlLoop *loop = c->loop;
430 CURL *e = c->easy;
431
432 curl_off_t recv = 0, time = 0;
433 curl_easy_getinfo(e, CURLINFO_SIZE_DOWNLOAD_T, &recv);
434 curl_easy_getinfo(e, CURLINFO_TOTAL_TIME_T, &time);
435
436 if (recv) {
437 av_log(c->h, AV_LOG_DEBUG, "%"PRId64" bytes received in %"PRId64" ms\n",
438 (int64_t) recv, (int64_t) time / 1000);
439
440 loop->total_bytes += recv;
441 loop->total_time_us += time;
442 }
443
444 long num_conns = 0, num_redirs = 0;
445 curl_easy_getinfo(e, CURLINFO_NUM_CONNECTS, &num_conns);
446 curl_easy_getinfo(e, CURLINFO_REDIRECT_COUNT, &num_redirs);
447 loop->num_connections += (int) num_conns;
448 loop->num_redirects += (int) num_redirs;
449 }
450
451 /* Transfer finished (or failed) */
452 static void on_done(CurlContext *c, CURLcode code)
453 {
454 int64_t received;
455 int aborted;
456
457 pthread_mutex_lock(&c->mutex);
458 aborted = c->aborted;
459 received = c->request_received;
460 /* Advance past delivered bytes so a retry or seek resumes at the right offset. */
461 if (received > INT64_MAX - c->request_start) {
462 if (!c->error)
463 c->error = AVERROR(EIO);
464 received = 0;
465 aborted = 1;
466 pthread_cond_broadcast(&c->cond);
467 }
468 c->request_start += received;
469 c->request_received = 0;
470 pthread_mutex_unlock(&c->mutex);
471 update_statistics(c);
472
473 if (!c->probed) {
474 /* Connection died before any usable header arrived. */
475 pthread_mutex_lock(&c->mutex);
476 c->probed = 1;
477 c->stream_ok = 0;
478 if (!c->error)
479 c->error = curlcode_to_averror(code);
480 pthread_cond_broadcast(&c->cond);
481 pthread_mutex_unlock(&c->mutex);
482 return;
483 }
484
485 if (code == CURLE_OK && !aborted && c->stream_ok) {
486 c->retry_count = 0;
487 int64_t file_end = c->content_size > 0 ? c->content_size - 1 : -1;
488 if (c->end_off > 0)
489 file_end = FFMIN(file_end, c->end_off - 1);
490 if (c->seekable && c->request_end >= 0 && c->request_end < file_end) {
491 c->is_initial = 0;
492 start_request(c);
493 return;
494 }
495 pthread_mutex_lock(&c->mutex);
496 c->eof = 1;
497 pthread_cond_broadcast(&c->cond);
498 pthread_mutex_unlock(&c->mutex);
499 return;
500 }
501
502 /* Resume seekable transfers after a recoverable error. */
503 if (!aborted && c->seekable && is_recoverable(code) &&
504 c->retry_count < c->max_retries) {
505 c->retry_count++;
506 c->loop->num_retries++;
507 av_log(c->h, AV_LOG_WARNING, "%s, retrying (#%d) from %"PRId64"\n",
508 curl_easy_strerror(code), c->retry_count, c->request_start);
509 start_request(c);
510 return;
511 }
512
513 if (!aborted) {
514 pthread_mutex_lock(&c->mutex);
515 if (!c->error)
516 c->error = curlcode_to_averror(code);
517 pthread_cond_broadcast(&c->cond);
518 pthread_mutex_unlock(&c->mutex);
519 }
520 }
521
522 /* ------------------------------------------------------------------------- */
523 /* event loop thread + command queue */
524 /* ------------------------------------------------------------------------- */
525
526 static void execute_command(CurlLoop *loop, CurlCmd *cmd)
527 {
528 CurlContext *c = cmd->ctx;
529
530 switch (cmd->kind) {
531 case CMD_ADD:
532 start_request(c);
533 break;
534 case CMD_REMOVE:
535 if (c->active) {
536 curl_multi_remove_handle(loop->multi, c->easy);
537 update_statistics(c);
538 c->active = 0;
539 }
540 break;
541 case CMD_UNPAUSE:
542 pthread_mutex_lock(&c->mutex);
543 c->paused = 0;
544 pthread_mutex_unlock(&c->mutex);
545 curl_easy_pause(c->easy, CURLPAUSE_CONT);
546 break;
547 case CMD_SEEK:
548 if (c->active) {
549 curl_multi_remove_handle(loop->multi, c->easy);
550 c->active = 0;
551 }
552 pthread_mutex_lock(&c->mutex);
553 av_fifo_reset2(c->fifo);
554 c->paused = 0;
555 c->eof = 0;
556 c->error = 0;
557 pthread_mutex_unlock(&c->mutex);
558 c->request_start = cmd->pos;
559 c->retry_count = 0;
560 start_request(c);
561 break;
562 }
563 }
564
565 static void *curl_worker(void *arg)
566 {
567 CurlLoop *loop = arg;
568
569 ff_thread_setname("curl");
570
571 while (1) {
572 CurlCmd *cmd;
573 CURLMsg *msg;
574 int running = 0, left = 0, do_exit;
575
576 pthread_mutex_lock(&loop->mutex);
577 cmd = loop->cmd_head;
578 if (cmd) {
579 loop->cmd_head = cmd->next;
580 if (!loop->cmd_head)
581 loop->cmd_tail = NULL;
582 }
583 do_exit = loop->exit;
584 pthread_mutex_unlock(&loop->mutex);
585
586 if (cmd) {
587 execute_command(loop, cmd);
588 if (cmd->sync) {
589 pthread_mutex_lock(&loop->mutex);
590 cmd->done = 1;
591 pthread_cond_broadcast(&loop->cond);
592 pthread_mutex_unlock(&loop->mutex);
593 } else {
594 av_free(cmd);
595 }
596 continue; /* drain the whole queue before pumping curl */
597 }
598
599 if (do_exit)
600 break;
601
602 curl_multi_perform(loop->multi, &running);
603
604 while ((msg = curl_multi_info_read(loop->multi, &left))) {
605 CurlContext *c = NULL;
606 if (msg->msg != CURLMSG_DONE)
607 continue;
608 curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &c);
609 curl_multi_remove_handle(loop->multi, msg->easy_handle);
610 if (c) {
611 c->active = 0;
612 on_done(c, msg->data.result);
613 }
614 }
615
616 curl_multi_poll(loop->multi, NULL, 0, 1000, NULL);
617 }
618
619 return NULL;
620 }
621
622 /* Dispatch a command to the loop. For sync commands the caller blocks until the
623 * loop thread has executed it. Returns 0 or a negative AVERROR. */
624 static int curl_dispatch(CurlLoop *loop, enum cmd_kind kind, CurlContext *c,
625 int64_t pos, int sync)
626 {
627 CurlCmd stackcmd = {0};
628 CurlCmd *cmd = sync ? &stackcmd : av_mallocz(sizeof(*cmd));
629
630 if (!cmd)
631 return AVERROR(ENOMEM);
632
633 cmd->kind = kind;
634 cmd->ctx = c;
635 cmd->pos = pos;
636 cmd->sync = sync;
637
638 pthread_mutex_lock(&loop->mutex);
639 if (loop->cmd_tail)
640 loop->cmd_tail->next = cmd;
641 else
642 loop->cmd_head = cmd;
643 loop->cmd_tail = cmd;
644 curl_multi_wakeup(loop->multi);
645 if (sync) {
646 while (!cmd->done)
647 pthread_cond_wait(&loop->cond, &loop->mutex);
648 }
649 pthread_mutex_unlock(&loop->mutex);
650
651 return 0;
652 }
653
654 static CurlLoop *curl_loop_create(AVFormatContext *avfc)
655 {
656 CurlLoop *loop = av_mallocz(sizeof(*loop));
657 if (!loop)
658 return NULL;
659 loop->avfc = avfc;
660
661 if (pthread_mutex_init(&loop->mutex, NULL))
662 goto fail;
663 if (pthread_cond_init(&loop->cond, NULL)) {
664 pthread_mutex_destroy(&loop->mutex);
665 goto fail;
666 }
667
668 if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
669 goto fail2;
670
671 loop->multi = curl_multi_init();
672 if (!loop->multi)
673 goto fail3;
674 curl_multi_setopt(loop->multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
675
676 loop->share = curl_share_init();
677 if (!loop->share)
678 goto fail3;
679 curl_share_setopt(loop->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
680 curl_share_setopt(loop->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS);
681
682 if (pthread_create(&loop->thread, NULL, curl_worker, loop))
683 goto fail3;
684
685 return loop;
686
687 fail3:
688 curl_multi_cleanup(loop->multi);
689 curl_share_cleanup(loop->share);
690 curl_global_cleanup();
691 fail2:
692 pthread_cond_destroy(&loop->cond);
693 pthread_mutex_destroy(&loop->mutex);
694 fail:
695 av_free(loop);
696 return NULL;
697 }
698
699 static void print_statistics(CurlLoop *loop)
700 {
701 AVFormatContext *avfc = loop->avfc;
702 if (!loop->total_bytes)
703 return;
704
705 double time = (double) loop->total_time_us / 1000000.0;
706 double avg = time ? loop->total_bytes / time : 0;
707 av_log(avfc, AV_LOG_VERBOSE,
708 "libcurl: Overall %"PRId64" bytes received in %.0f ms = %.0f kB/s\n",
709 loop->total_bytes, time * 1e3, avg / 1e3);
710
711 av_log(avfc, AV_LOG_VERBOSE,
712 "libcurl: %d connections, %d redirects, %d requests, %d retries\n",
713 loop->num_connections, loop->num_redirects, loop->num_requests, loop->num_retries);
714 }
715
716 static void curl_loop_destroy(CurlLoop *loop)
717 {
718 pthread_mutex_lock(&loop->mutex);
719 loop->exit = 1;
720 curl_multi_wakeup(loop->multi);
721 pthread_mutex_unlock(&loop->mutex);
722
723 pthread_join(loop->thread, NULL);
724 print_statistics(loop);
725
726 curl_multi_cleanup(loop->multi);
727 curl_share_cleanup(loop->share);
728 pthread_cond_destroy(&loop->cond);
729 pthread_mutex_destroy(&loop->mutex);
730 av_free(loop);
731
732 /* Released after the thread is joined and the multi handle is gone. */
733 curl_global_cleanup();
734 }
735
736 /* Attach a context to its event loop. With an owning AVFormatContext the loop is
737 * created lazily, cached on it, and shared across the demuxer's transfers so curl
738 * reuses connections; it is freed at format teardown. Without one the context
739 * gets a private loop freed on close. */
740 static int curl_loop_attach(CurlContext *c, AVFormatContext *avfc)
741 {
742 if (!avfc) {
743 c->loop = curl_loop_create(NULL);
744 c->private_loop = 1;
745 return c->loop ? 0 : AVERROR(ENOMEM);
746 }
747
748 pthread_mutex_lock(&curl_loop_lock);
749 c->loop = ffformatcontext(avfc)->curl_loop;
750 if (!c->loop) {
751 c->loop = curl_loop_create(avfc);
752 ffformatcontext(avfc)->curl_loop = c->loop;
753 }
754 pthread_mutex_unlock(&curl_loop_lock);
755
756 return c->loop ? 0 : AVERROR(ENOMEM);
757 }
758
759 17035 void ff_curl_loop_free(struct CurlLoop **loop)
760 {
761
2/4
✓ Branch 0 taken 17035 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 17035 times.
17035 if (loop && *loop) {
762 curl_loop_destroy(*loop);
763 *loop = NULL;
764 }
765 17035 }
766
767 /* ------------------------------------------------------------------------- */
768 /* URLProtocol callbacks */
769 /* ------------------------------------------------------------------------- */
770
771 static int libcurl_close(URLContext *h);
772
773 static int debug_callback(CURL *easy, curl_infotype type, char *data,
774 size_t size, void *userdata)
775 {
776 CurlContext *c = userdata;
777 const char *prefix, *p = data, *end = data + size;
778
779 switch (type) {
780 case CURLINFO_TEXT: prefix = "* "; break;
781 case CURLINFO_HEADER_IN: prefix = "< "; break;
782 case CURLINFO_HEADER_OUT: prefix = "> "; break;
783 default: return 0;
784 }
785
786 /* Split multiline payload into each log. */
787 while (p < end) {
788 const char *nl = memchr(p, '\n', end - p);
789 size_t len = (nl ? nl : end) - p;
790 while (len && p[len - 1] == '\r')
791 len--;
792 av_log(c->h, AV_LOG_DEBUG, "%s%.*s\n", prefix, (int)len, p);
793 if (!nl)
794 break;
795 p = nl + 1;
796 }
797 return 0;
798 }
799
800 /* Build the custom request header list from the referer and headers options. */
801 static struct curl_slist *build_headers(CurlContext *c)
802 {
803 struct curl_slist *list = NULL;
804
805 if (c->referer && c->referer[0]) {
806 char *h = av_asprintf("Referer: %s", c->referer);
807 if (h) {
808 list = curl_slist_append(list, h);
809 av_free(h);
810 }
811 }
812 if (c->headers && c->headers[0]) {
813 char *copy = av_strdup(c->headers);
814 char *line, *saveptr = NULL;
815 if (copy) {
816 for (line = av_strtok(copy, "\r\n", &saveptr); line;
817 line = av_strtok(NULL, "\r\n", &saveptr))
818 list = curl_slist_append(list, line);
819 av_free(copy);
820 }
821 }
822 return list;
823 }
824
825 static int setup_protocols(CurlContext *c)
826 {
827 const char *wl = c->h->protocol_whitelist;
828 const char *bl = c->h->protocol_blacklist;
829 if (!wl && !bl)
830 return 0;
831
832 AVBPrint bp;
833 av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC);
834
835 curl_version_info_data *info = curl_version_info(CURLVERSION_NOW);
836 for (const char *const *p = info->protocols; *p; p++) {
837 const char *proto = *p;
838 if (av_strcasecmp(proto, "http") && av_strcasecmp(proto, "https"))
839 continue; /* only http(s) are supported by libcurl.c at the moment */
840 if (wl && av_match_list(proto, wl, ',') <= 0)
841 continue;
842 if (bl && av_match_list(proto, bl, ',') > 0)
843 continue;
844 if (bp.len)
845 av_bprint_chars(&bp, ',', 1);
846 av_bprintf(&bp, "%s", proto);
847 }
848
849 if (!av_bprint_is_complete(&bp)) {
850 av_bprint_finalize(&bp, NULL);
851 return AVERROR(ENOMEM);
852 }
853
854 if (!bp.len) {
855 av_log(c->h, AV_LOG_ERROR, "Set of allowed protocols is empty.\n");
856 av_bprint_finalize(&bp, NULL);
857 return AVERROR(EINVAL);
858 }
859
860 curl_easy_setopt(c->easy, CURLOPT_PROTOCOLS_STR, bp.str);
861 curl_easy_setopt(c->easy, CURLOPT_REDIR_PROTOCOLS_STR, bp.str);
862 av_bprint_finalize(&bp, NULL);
863 return 0;
864 }
865
866 static void setup_curl(CurlContext *c)
867 {
868 CURL *e = c->easy;
869 const char *url = c->h->filename;
870
871 /* Drop an optional "libcurl:" prefix that forces this protocol. */
872 av_strstart(url, "libcurl:", &url);
873
874 curl_easy_setopt(e, CURLOPT_URL, url);
875 curl_easy_setopt(e, CURLOPT_PRIVATE, c);
876 curl_easy_setopt(e, CURLOPT_NOSIGNAL, 1L);
877 curl_easy_setopt(e, CURLOPT_SHARE, c->loop->share);
878
879 curl_easy_setopt(e, CURLOPT_WRITEFUNCTION, write_callback);
880 curl_easy_setopt(e, CURLOPT_WRITEDATA, c);
881 curl_easy_setopt(e, CURLOPT_HEADERFUNCTION, header_callback);
882 curl_easy_setopt(e, CURLOPT_HEADERDATA, c);
883
884 curl_easy_setopt(e, CURLOPT_NOPROGRESS, 0L);
885 curl_easy_setopt(e, CURLOPT_XFERINFOFUNCTION, xferinfo_callback);
886 curl_easy_setopt(e, CURLOPT_XFERINFODATA, c);
887
888 if (av_log_get_level() >= AV_LOG_DEBUG) {
889 curl_easy_setopt(e, CURLOPT_VERBOSE, 1L);
890 curl_easy_setopt(e, CURLOPT_DEBUGFUNCTION, debug_callback);
891 curl_easy_setopt(e, CURLOPT_DEBUGDATA, c);
892 }
893
894 curl_easy_setopt(e, CURLOPT_FOLLOWLOCATION, 1L);
895 curl_easy_setopt(e, CURLOPT_MAXREDIRS, (long)c->max_redirects);
896 curl_easy_setopt(e, CURLOPT_HTTP_VERSION, (long)c->http_version);
897 curl_easy_setopt(e, CURLOPT_TCP_KEEPALIVE, c->multiple_requests ? 1L : 0L);
898 curl_easy_setopt(e, CURLOPT_FORBID_REUSE, c->multiple_requests ? 0L : 1L);
899 curl_easy_setopt(e, CURLOPT_HSTS_CTRL, (long)CURLHSTS_ENABLE);
900 curl_easy_setopt(e, CURLOPT_ACCEPT_ENCODING,
901 c->off > 0 || c->end_off > 0 ? "identity" : "");
902 if (c->connect_timeout > 0)
903 curl_easy_setopt(e, CURLOPT_CONNECTTIMEOUT_MS,
904 (long)c->connect_timeout * 1000);
905
906 if (c->user_agent && c->user_agent[0])
907 curl_easy_setopt(e, CURLOPT_USERAGENT, c->user_agent);
908 if (c->http_proxy && c->http_proxy[0])
909 curl_easy_setopt(e, CURLOPT_PROXY, c->http_proxy);
910
911 curl_easy_setopt(e, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NATIVE_CA);
912 curl_easy_setopt(e, CURLOPT_SSL_VERIFYPEER, c->tls_verify ? 1L : 0L);
913 curl_easy_setopt(e, CURLOPT_SSL_VERIFYHOST, c->tls_verify ? 2L : 0L);
914 if (c->ca_file)
915 curl_easy_setopt(e, CURLOPT_CAINFO, c->ca_file);
916 if (c->cert_file)
917 curl_easy_setopt(e, CURLOPT_SSLCERT, c->cert_file);
918 if (c->key_file)
919 curl_easy_setopt(e, CURLOPT_SSLKEY, c->key_file);
920
921 curl_easy_setopt(e, CURLOPT_COOKIEFILE, "");
922 if (c->cookies && c->cookies[0]) {
923 char *copy = av_strdup(c->cookies);
924 char *line, *saveptr = NULL;
925 if (copy) {
926 for (line = av_strtok(copy, "\r\n", &saveptr); line;
927 line = av_strtok(NULL, "\r\n", &saveptr)) {
928 char *sc = av_asprintf("Set-Cookie: %s", line);
929 if (sc) {
930 curl_easy_setopt(e, CURLOPT_COOKIELIST, sc);
931 av_free(sc);
932 }
933 }
934 av_free(copy);
935 }
936 }
937
938 c->header_list = build_headers(c);
939 if (c->header_list)
940 curl_easy_setopt(e, CURLOPT_HTTPHEADER, c->header_list);
941 }
942
943 static void curl_cond_wait(CurlContext *c)
944 {
945 int64_t t = av_gettime() + CURL_WAIT_US;
946 struct timespec ts = { .tv_sec = t / 1000000,
947 .tv_nsec = (t % 1000000) * 1000 };
948 pthread_cond_timedwait(&c->cond, &c->mutex, &ts);
949 }
950
951 /* Block until the transfer has been probed, the stream errored, or the open was
952 * interrupted. Returns 0, or a negative AVERROR. */
953 static int wait_for_probe(CurlContext *c)
954 {
955 URLContext *h = c->h;
956 int ret = 0;
957
958 pthread_mutex_lock(&c->mutex);
959 while (!c->probed && !c->error) {
960 if (ff_check_interrupt(&h->interrupt_callback)) {
961 c->aborted = 1;
962 ret = AVERROR_EXIT;
963 break;
964 }
965 curl_cond_wait(c);
966 }
967 if (!ret) {
968 if (!c->stream_ok)
969 ret = c->error ? c->error : AVERROR(EIO);
970 }
971 pthread_mutex_unlock(&c->mutex);
972
973 return ret;
974 }
975
976 static int libcurl_open(URLContext *h, const char *url, int flags,
977 AVDictionary **options)
978 {
979 /* Guard against non-thread-safe libcurl builds. This should never happen,
980 * since libcurl is used only on platforms with thread support, and thread
981 * safety is enabled unconditionally in libcurl when the platform supports
982 * threads or atomics. */
983 curl_version_info_data *info = curl_version_info(CURLVERSION_NOW);
984 if (!(info->features & CURL_VERSION_THREADSAFE))
985 return AVERROR(ENOSYS);
986
987 CurlContext *c = h->priv_data;
988 const char *eff_url = h->filename;
989 int ret;
990
991 c->h = h;
992 c->content_size = -1;
993 c->request_start = c->off;
994 c->request_end = -1;
995 c->logical_pos = c->off;
996 c->is_initial = 1;
997
998 /* Report the request URL until header_callback replaces it post-redirect. */
999 av_strstart(eff_url, "libcurl:", &eff_url);
1000 av_freep(&c->location);
1001 c->location = av_strdup(eff_url);
1002
1003 ret = pthread_mutex_init(&c->mutex, NULL);
1004 if (ret)
1005 return AVERROR(ret);
1006 ret = pthread_cond_init(&c->cond, NULL);
1007 if (ret) {
1008 pthread_mutex_destroy(&c->mutex);
1009 return AVERROR(ret);
1010 }
1011
1012 c->fifo = av_fifo_alloc2(c->buffer_size, 1, 0);
1013 if (!c->fifo) {
1014 ret = AVERROR(ENOMEM);
1015 goto fail;
1016 }
1017
1018 ret = curl_loop_attach(c, h->avfc);
1019 if (ret < 0)
1020 goto fail;
1021
1022 c->easy = curl_easy_init();
1023 if (!c->easy) {
1024 ret = AVERROR(ENOMEM);
1025 goto fail;
1026 }
1027
1028 ret = setup_protocols(c);
1029 if (ret < 0)
1030 goto fail;
1031
1032 setup_curl(c);
1033
1034 ret = curl_dispatch(c->loop, CMD_ADD, c, 0, 0);
1035 if (ret < 0)
1036 goto fail;
1037
1038 ret = wait_for_probe(c);
1039 if (ret < 0)
1040 goto fail;
1041
1042 pthread_mutex_lock(&c->mutex);
1043 h->is_streamed = !c->seekable;
1044 pthread_mutex_unlock(&c->mutex);
1045
1046 return 0;
1047
1048 fail:
1049 libcurl_close(h);
1050 return ret;
1051 }
1052
1053 static int libcurl_read(URLContext *h, unsigned char *buf, int size)
1054 {
1055 CurlContext *c = h->priv_data;
1056 int nonblock = h->flags & AVIO_FLAG_NONBLOCK;
1057 int ret;
1058
1059 pthread_mutex_lock(&c->mutex);
1060 while (1) {
1061 size_t avail = av_fifo_can_read(c->fifo);
1062
1063 if (avail) {
1064 int n = FFMIN(avail, (size_t)size);
1065 int unpause;
1066 av_fifo_read(c->fifo, buf, n);
1067 /* Resume a paused transfer once the FIFO is at least half empty. */
1068 unpause = c->paused && av_fifo_can_write(c->fifo) * 2 >= c->buffer_size;
1069 c->logical_pos += n;
1070 pthread_mutex_unlock(&c->mutex);
1071 if (unpause)
1072 curl_dispatch(c->loop, CMD_UNPAUSE, c, 0, 0);
1073 return n;
1074 }
1075 if (c->error) {
1076 ret = c->error;
1077 break;
1078 }
1079 if (c->eof) {
1080 ret = AVERROR_EOF;
1081 break;
1082 }
1083 if (nonblock) {
1084 ret = AVERROR(EAGAIN);
1085 break;
1086 }
1087 curl_cond_wait(c);
1088 /* Return to the avio layer so it can poll the interrupt callback. */
1089 nonblock = 1;
1090 }
1091 pthread_mutex_unlock(&c->mutex);
1092
1093 return ret;
1094 }
1095
1096 static int64_t libcurl_seek(URLContext *h, int64_t pos, int whence)
1097 {
1098 CurlContext *c = h->priv_data;
1099 int64_t newpos;
1100
1101 pthread_mutex_lock(&c->mutex);
1102 const int64_t content_size = c->content_size;
1103 const int seekable = c->seekable;
1104 pthread_mutex_unlock(&c->mutex);
1105
1106 if (whence == AVSEEK_SIZE)
1107 return content_size >= 0 ? content_size : AVERROR(ENOSYS);
1108
1109 if (!seekable)
1110 return AVERROR(ENOSYS);
1111
1112 switch (whence) {
1113 case SEEK_SET:
1114 newpos = pos;
1115 break;
1116 case SEEK_CUR:
1117 if (pos > INT64_MAX - c->logical_pos)
1118 return AVERROR(ERANGE);
1119 newpos = c->logical_pos + pos;
1120 break;
1121 case SEEK_END:
1122 if (content_size < 0)
1123 return AVERROR(ENOSYS);
1124 if (pos > INT64_MAX - content_size)
1125 return AVERROR(ERANGE);
1126 newpos = content_size + pos;
1127 break;
1128 default:
1129 return AVERROR(EINVAL);
1130 }
1131 if (newpos < 0)
1132 return AVERROR(EINVAL);
1133
1134 if (newpos == c->logical_pos)
1135 return newpos;
1136
1137 /* Restart the transfer at the new offset. Any failure of the new request
1138 * surfaces on the following url_read(). */
1139 curl_dispatch(c->loop, CMD_SEEK, c, newpos, 1);
1140 c->logical_pos = newpos;
1141
1142 return newpos;
1143 }
1144
1145 static int libcurl_close(URLContext *h)
1146 {
1147 CurlContext *c = h->priv_data;
1148
1149 if (c->loop) {
1150 if (c->easy) {
1151 /* Ensure the handle is out of the multi before we free it. */
1152 curl_dispatch(c->loop, CMD_REMOVE, c, 0, 1);
1153 curl_easy_cleanup(c->easy);
1154 c->easy = NULL;
1155 }
1156 /* A shared loop outlives the transfer for connection reuse. */
1157 if (c->private_loop)
1158 curl_loop_destroy(c->loop);
1159 c->loop = NULL;
1160 }
1161
1162 if (c->header_list)
1163 curl_slist_free_all(c->header_list);
1164 av_fifo_freep2(&c->fifo);
1165 pthread_cond_destroy(&c->cond);
1166 pthread_mutex_destroy(&c->mutex);
1167
1168 return 0;
1169 }
1170
1171 #define OFFSET(x) offsetof(CurlContext, x)
1172 #define D AV_OPT_FLAG_DECODING_PARAM
1173 #define E AV_OPT_FLAG_ENCODING_PARAM
1174 static const AVOption options[] = {
1175 { "user_agent", "override User-Agent header", OFFSET(user_agent), AV_OPT_TYPE_STRING, { .str = DEFAULT_USER_AGENT }, 0, 0, D },
1176 { "referer", "override Referer header", OFFSET(referer), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D },
1177 { "headers", "set custom HTTP headers, can override built in default headers", OFFSET(headers), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E },
1178 { "http_proxy", "set HTTP proxy to tunnel through", OFFSET(http_proxy), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E },
1179 { "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 },
1180 { "location", "the actual location of the data received", OFFSET(location), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E },
1181 { "offset", "initial byte offset", OFFSET(off), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D },
1182 { "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 },
1183 { "seekable", "control seekability of connection", OFFSET(seekable_opt), AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, D },
1184 { "tls_verify", "verify the peer certificate and hostname", OFFSET(tls_verify), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D | E },
1185 { "ca_file", "certificate authority bundle file", OFFSET(ca_file), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E },
1186 { "cert_file", "client certificate file", OFFSET(cert_file), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E },
1187 { "key_file", "client private key file", OFFSET(key_file), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D | E },
1188 { "connect_timeout", "connection timeout in seconds (0 = libcurl default)", OFFSET(connect_timeout), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX / 1000, D | E },
1189 { "max_redirects", "maximum number of redirects to follow", OFFSET(max_redirects), AV_OPT_TYPE_INT, { .i64 = 16 }, 0, INT_MAX, D },
1190 { "multiple_requests", "reuse the connection across requests (HTTP keep-alive)", OFFSET(multiple_requests), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D | E },
1191 { "max_retries", "maximum number of retries after a recoverable error", OFFSET(max_retries), AV_OPT_TYPE_INT, { .i64 = 5 }, 0, INT_MAX, D },
1192 { "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 },
1193 { "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 },
1194 { "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 },
1195 { "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" },
1196 { "auto", "negotiate the best supported version", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_NONE }, 0, 0, D, .unit = "http_version" },
1197 { "1.0", "HTTP/1.0", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_1_0 }, 0, 0, D, .unit = "http_version" },
1198 { "1.1", "HTTP/1.1", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_1_1 }, 0, 0, D, .unit = "http_version" },
1199 { "2", "HTTP/2", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_2 }, 0, 0, D, .unit = "http_version" },
1200 { "2tls", "HTTP/2 over TLS only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_2TLS }, 0, 0, D, .unit = "http_version" },
1201 { "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" },
1202 { "3", "HTTP/3, fall back to earlier versions", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3 }, 0, 0, D, .unit = "http_version" },
1203 { "3only", "HTTP/3 only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3ONLY }, 0, 0, D, .unit = "http_version" },
1204 { NULL }
1205 };
1206
1207 static const AVClass libcurl_context_class = {
1208 .class_name = "libcurl",
1209 .item_name = av_default_item_name,
1210 .option = options,
1211 .version = LIBAVUTIL_VERSION_INT,
1212 };
1213
1214 const URLProtocol ff_libcurl_protocol = {
1215 .name = "libcurl",
1216 .url_open2 = libcurl_open,
1217 .url_read = libcurl_read,
1218 .url_seek = libcurl_seek,
1219 .url_close = libcurl_close,
1220 .priv_data_size = sizeof(CurlContext),
1221 .priv_data_class = &libcurl_context_class,
1222 .flags = URL_PROTOCOL_FLAG_NETWORK,
1223 .default_whitelist = "http,https,libcurl",
1224 };
1225