Add an http3:// URLProtocol: a read client that fetches resources over
HTTP/3 (RFC 9114) / QUIC (RFC 9000) instead of TCP, using ngtcp2 for the
QUIC transport and nghttp3 for HTTP/3. It supports HTTP Range (seek),
3xx redirect following, and a process-global host-keyed connection pool
with QUIC keep-alive so consecutive requests (e.g. HLS segments) reuse a
connection instead of re-handshaking. The "altsvc" option discovers the
HTTP/3 endpoint via an Alt-Svc probe instead of assuming prior knowledge.
QUIC needs a TLS library with a QUIC interface. The crypto backend follows
FFmpeg's own TLS selection via the ngtcp2 crypto helpers: GnuTLS with
--enable-gnutls, otherwise the OpenSSL-family helper -- BoringSSL
(OPENSSL_IS_BORINGSSL) or OpenSSL 3.5+ native QUIC (the ossl helper).
configure picks the matching libngtcp2_crypto_* and only pulls the C++
runtime for BoringSSL. The connection bring-up, CSPRNG, native TLS handle
and Alt-Svc probe are selected per backend behind #if.
Enable with --enable-libngtcp2 --enable-libnghttp3 and one TLS backend.
Verified end-to-end (HTTP/3 GET -> 200 and MP4 demux over Range) against
all three backends: GnuTLS, BoringSSL and OpenSSL 3.5.
---
Changelog | 1 +
configure | 28 +
doc/protocols.texi | 27 +
libavformat/Makefile | 1 +
libavformat/http3.c | 1165 +++++++++++++++++++++++++++++++++++++++
libavformat/protocols.c | 1 +
6 files changed, 1223 insertions(+)
create mode 100644 libavformat/http3.c
diff --git a/Changelog b/Changelog
index 3c48005..29d1821 100644
--- a/Changelog
+++ b/Changelog
@@ -2,6 +2,7 @@ Entries are sorted chronologically from oldest to youngest
within each release,
releases are sorted from youngest to oldest.
version <next>:
+- HTTP/3 (QUIC) input protocol
- Extend AMF Color Converter (vf_vpp_amf) HDR capabilities
- LCEVC track muxing support in MP4 muxer
- Playdate video encoder and muxer
diff --git a/configure b/configure
index e67aa36..44e9974 100755
--- a/configure
+++ b/configure
@@ -2121,6 +2121,8 @@ EXTERNAL_LIBRARY_LIST="
libquirc
librabbitmq
librav1e
+ libnghttp3
+ libngtcp2
librist
librsvg
librtmp
@@ -4141,6 +4143,8 @@ libamqp_protocol_deps="librabbitmq"
libamqp_protocol_select="network"
librist_protocol_deps="librist"
librist_protocol_select="network"
+http3_protocol_deps="libngtcp2 libnghttp3"
+http3_protocol_select="network"
librtmp_protocol_deps="librtmp"
librtmpe_protocol_deps="librtmp"
librtmps_protocol_deps="librtmp"
@@ -7456,6 +7460,30 @@ enabled libquirc && require libquirc quirc.h
quirc_decode -lquirc
enabled librabbitmq && require_pkg_config librabbitmq "librabbitmq >=
0.7.1" amqp.h amqp_new_connection
enabled librav1e && require_pkg_config librav1e "rav1e >= 0.5.0"
rav1e.h rav1e_context_new
enabled librist && require_pkg_config librist "librist >= 0.2.7"
librist/librist.h rist_receiver_create
+enabled libnghttp3 && require_pkg_config libnghttp3 libnghttp3
nghttp3/nghttp3.h nghttp3_version
+enabled libngtcp2 && require_pkg_config libngtcp2 libngtcp2
ngtcp2/ngtcp2.h ngtcp2_version && {
+ # http3.c selects its crypto backend by CONFIG_GNUTLS; link the matching
+ # ngtcp2 crypto helper. The OpenSSL-family path is validated against
+ # BoringSSL (C++); its headers/libs come from the TLS enablement or the
+ # user's --extra-cflags/--extra-libs, never a hardcoded install prefix.
+ # quictls / OpenSSL 3.5 native QUIC (ossl helper) is a documented
follow-up.
+ if enabled gnutls; then
+ append libngtcp2_extralibs "-lngtcp2_crypto_gnutls"
+ # http3.c + the gnutls crypto helper call gnutls directly; make sure
+ # gnutls is on the link line even if no other gnutls user is built
+ append libngtcp2_extralibs $($pkg_config --libs gnutls 2>/dev/null ||
echo -lgnutls)
+ elif check_cpp_condition ngtcp2_boringssl openssl/crypto.h
"defined(OPENSSL_IS_BORINGSSL)"; then
+ append libngtcp2_extralibs "-lngtcp2_crypto_boringssl"
+ # BoringSSL is C++
+ if test "$target_os" = darwin; then
+ append libngtcp2_extralibs "-lc++"
+ else
+ append libngtcp2_extralibs "-lstdc++"
+ fi
+ else
+ # OpenSSL 3.5+ native QUIC via the ngtcp2 ossl helper (C, no C++
runtime)
+ append libngtcp2_extralibs "-lngtcp2_crypto_ossl"
+ fi; }
enabled librsvg && require_pkg_config librsvg librsvg-2.0
librsvg-2.0/librsvg/rsvg.h rsvg_handle_new_from_data
enabled librtmp && require_pkg_config librtmp librtmp librtmp/rtmp.h
RTMP_Socket
enabled librubberband && require_pkg_config librubberband "rubberband >=
1.8.1" rubberband/rubberband-c.h rubberband_new -lstdc++ && append
librubberband_extralibs "-lstdc++"
diff --git a/doc/protocols.texi b/doc/protocols.texi
index a5ebd98..1fbcc23 100644
--- a/doc/protocols.texi
+++ b/doc/protocols.texi
@@ -642,6 +642,33 @@ The required syntax to play a stream specifying a cookie
is:
ffplay -cookies "nlqptid=nltid=tsn; path=/; domain=somedomain.com;"
http://somedomain.com/somestream.m3u8
@end example
+@section http3
+
+HTTP/3 (RFC 9114): HTTP semantics carried over QUIC (RFC 9000) instead of TCP.
+This is a read-only client protocol for fetching resources (for example HLS
+segments or progressive media) over QUIC, with HTTP @code{Range} support for
+seeking and connection reuse across requests to the same host.
+
+QUIC needs a TLS library that exposes a QUIC interface. The crypto backend
+follows FFmpeg's own TLS selection: configure with @code{--enable-gnutls} to
use
+GnuTLS, otherwise the OpenSSL-family backend is used -- either BoringSSL or
+OpenSSL 3.5+ (native QUIC). The protocol additionally requires
+@code{--enable-libngtcp2} (QUIC
+transport) and @code{--enable-libnghttp3} (HTTP/3 framing).
+
+By default the server is assumed to speak HTTP/3 on the requested (or default
+443) UDP port. Use the @option{altsvc} option to instead probe the origin over
+HTTPS first and follow the port it advertises in its @code{Alt-Svc} header.
+
+This protocol accepts the following options:
+
+@table @option
+@item altsvc
+If set to 1, before connecting, probe the origin over HTTPS (HTTP/1.1) and use
+the HTTP/3 authority advertised in its @code{Alt-Svc} response header instead
of
+assuming HTTP/3 on the requested port. Default value is 0.
+@end table
+
@section Icecast
Icecast protocol (stream to Icecast servers)
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 0db0c7c..72fc4fe 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -740,6 +740,7 @@ OBJS-$(CONFIG_LIBRTMPS_PROTOCOL) += librtmp.o
OBJS-$(CONFIG_LIBRTMPT_PROTOCOL) += librtmp.o
OBJS-$(CONFIG_LIBRTMPTE_PROTOCOL) += librtmp.o
OBJS-$(CONFIG_LIBSMBCLIENT_PROTOCOL) += libsmbclient.o
+OBJS-$(CONFIG_HTTP3_PROTOCOL) += http3.o
OBJS-$(CONFIG_LIBSRT_PROTOCOL) += libsrt.o
OBJS-$(CONFIG_LIBSSH_PROTOCOL) += libssh.o
OBJS-$(CONFIG_LIBZMQ_PROTOCOL) += libzmq.o
diff --git a/libavformat/http3.c b/libavformat/http3.c
new file mode 100644
index 0000000..121d221
--- /dev/null
+++ b/libavformat/http3.c
@@ -0,0 +1,1165 @@
+/*
+ * HTTP/3 (QUIC) protocol for FFmpeg.
+ *
+ * Copyright (c) 2026 Simon Christ
+ *
+ * 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.
+ */
+
+/*
+ * QUIC transport (ngtcp2) + HTTP/3 client (nghttp3): seekable GETs with Range,
+ * response-status handling and redirect following. The TLS crypto backend
+ * follows FFmpeg's TLS selection: GnuTLS with --enable-gnutls, otherwise the
+ * OpenSSL-family helper -- BoringSSL (OPENSSL_IS_BORINGSSL, the iOS/mobile
+ * path) or OpenSSL 3.5+ native QUIC (the ossl helper). All three verified.
+ *
+ * Connection vs request split: the QUIC/H3 connection lives in a heap H3Conn
+ * (stable address — ngtcp2/nghttp3/the TLS lib store a pointer to it as their
+ * user_data, and there is no set_user_data to retarget after creation). The
+ * per-URLContext request state lives in HTTP3Context; H3Conn->cur points at
the
+ * request that currently owns the connection. That indirection is what lets a
+ * connection be parked in a process-global pool and reused by a later
+ * URLContext (e.g. consecutive HLS segments on the same host) without paying a
+ * fresh QUIC handshake.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <netdb.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "config.h"
+
+/* QUIC needs a TLS library with a QUIC interface. The crypto backend follows
+ FFmpeg's own TLS selection: --enable-gnutls -> GnuTLS, otherwise the
+ OpenSSL-family path (validated against BoringSSL, which is the only
+ OpenSSL-API TLS with a QUIC interface usable on e.g. iOS). quictls/OpenSSL
+ 3.5 native QUIC is a documented follow-up. */
+#if CONFIG_GNUTLS
+#include <gnutls/gnutls.h>
+#include <gnutls/crypto.h>
+#include <ngtcp2/ngtcp2_crypto_gnutls.h>
+#else
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#if defined(OPENSSL_IS_BORINGSSL)
+#include <ngtcp2/ngtcp2_crypto_boringssl.h>
+#else
+#include <ngtcp2/ngtcp2_crypto_ossl.h>
+#endif
+#endif
+#include <ngtcp2/ngtcp2.h>
+#include <ngtcp2/ngtcp2_crypto.h>
+#include <nghttp3/nghttp3.h>
+
+#include "libavutil/avstring.h"
+#include "libavutil/error.h"
+#include "libavutil/log.h"
+#include "libavutil/mem.h"
+#include "libavutil/opt.h"
+#include "libavutil/thread.h"
+#include "libavutil/time.h"
+#include "avformat.h"
+#include "url.h"
+
+#define H3_ALPN "h3"
+#define H3_DGRAM 65536
+
+typedef struct HTTP3Context HTTP3Context;
+
+/* Connection-level, poolable, stable heap address. */
+typedef struct H3Conn {
+ int fd;
+ ngtcp2_conn *conn;
+ nghttp3_conn *h3conn;
+ ngtcp2_crypto_conn_ref conn_ref;
+#if CONFIG_GNUTLS
+ gnutls_session_t session;
+ gnutls_certificate_credentials_t cred;
+#else
+ SSL_CTX *ssl_ctx;
+ SSL *ssl;
+#if !defined(OPENSSL_IS_BORINGSSL)
+ ngtcp2_crypto_ossl_ctx *ossl_ctx; /* OpenSSL 3.5 native QUIC */
+#endif
+#endif
+
+ struct sockaddr_storage local_addr, remote_addr;
+ socklen_t local_addrlen, remote_addrlen;
+ uint8_t sr_secret[32];
+
+ char host[1024];
+ int port;
+
+ HTTP3Context *cur; /* request currently driving this connection */
+} H3Conn;
+
+/* native TLS handle ngtcp2 drives, per backend */
+#if CONFIG_GNUTLS
+#define H3_TLS_HANDLE(hc) ((hc)->session)
+#elif defined(OPENSSL_IS_BORINGSSL)
+#define H3_TLS_HANDLE(hc) ((hc)->ssl)
+#else
+#define H3_TLS_HANDLE(hc) ((hc)->ossl_ctx)
+#endif
+
+#if !CONFIG_GNUTLS && !defined(OPENSSL_IS_BORINGSSL)
+static AVOnce h3_ossl_once = AV_ONCE_INIT;
+static void h3_ossl_global_init(void) { ngtcp2_crypto_ossl_init(); }
+#endif
+
+/* Per-URLContext request state. */
+struct HTTP3Context {
+ const AVClass *class;
+ H3Conn *hc;
+
+ char path[2048];
+
+ int64_t stream_id;
+ int stream_done;
+ int status;
+ int headers_done;
+ char location[2048];
+
+ int64_t off;
+ int64_t filesize;
+
+ unsigned char *rb;
+ size_t rb_size, rb_len, rb_off;
+ size_t total_recv;
+
+ int64_t open_timeout_us;
+ int altsvc; /* AVOption: discover h3 via Alt-Svc before
connecting */
+};
+
+/* ---- connection pool (host-keyed, N slots, FIFO eviction) ---- */
+
+#define H3_POOL_MAX 8
+static AVMutex h3_pool_mutex = AV_MUTEX_INITIALIZER;
+static H3Conn *h3_pool[H3_POOL_MAX];
+
+/* ---- helpers ---- */
+
+static uint64_t h3_timestamp(void)
+{
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return (uint64_t)t.tv_sec * NGTCP2_SECONDS + (uint64_t)t.tv_nsec;
+}
+
+static ngtcp2_conn *h3_get_conn(ngtcp2_crypto_conn_ref *ref)
+{
+ return ((H3Conn *)ref->user_data)->conn;
+}
+
+/* backend-neutral CSPRNG; 0 on success, <0 on failure */
+static int h3_random(uint8_t *dst, size_t len)
+{
+#if CONFIG_GNUTLS
+ return gnutls_rnd(GNUTLS_RND_RANDOM, dst, len) == 0 ? 0 : -1;
+#else
+ return RAND_bytes(dst, len) == 1 ? 0 : -1;
+#endif
+}
+
+static int h3_buf_append(HTTP3Context *c, const uint8_t *data, size_t len)
+{
+ if (c->rb_len + len > c->rb_size) {
+ size_t ns = FFMAX(c->rb_size * 2, c->rb_len + len);
+ unsigned char *nb = av_realloc(c->rb, ns);
+ if (!nb)
+ return AVERROR(ENOMEM);
+ c->rb = nb;
+ c->rb_size = ns;
+ }
+ memcpy(c->rb + c->rb_len, data, len);
+ c->rb_len += len;
+ c->total_recv += len;
+ return 0;
+}
+
+/* ---- ngtcp2 callbacks (user_data = H3Conn*) ---- */
+
+static void h3_rand_cb(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx
*ctx)
+{
+ h3_random(dest, destlen);
+}
+
+static int h3_get_new_cid_cb(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t
*token,
+ size_t cidlen, void *user_data)
+{
+ H3Conn *hc = user_data;
+ if (h3_random(cid->data, cidlen) < 0)
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ cid->datalen = cidlen;
+ if (ngtcp2_crypto_generate_stateless_reset_token(
+ token, hc->sr_secret, sizeof(hc->sr_secret), cid) != 0)
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ return 0;
+}
+
+static int h3_recv_stream_data_cb(ngtcp2_conn *conn, uint32_t flags,
+ int64_t stream_id, uint64_t offset,
+ const uint8_t *data, size_t datalen,
+ void *user_data, void *stream_user_data)
+{
+ H3Conn *hc = user_data;
+ nghttp3_ssize n;
+ if (!hc->h3conn)
+ return 0;
+ n = nghttp3_conn_read_stream(hc->h3conn, stream_id, data, datalen,
+ flags & NGTCP2_STREAM_DATA_FLAG_FIN);
+ if (n < 0)
+ return NGTCP2_ERR_CALLBACK_FAILURE;
+ ngtcp2_conn_extend_max_stream_offset(conn, stream_id, (uint64_t)n);
+ ngtcp2_conn_extend_max_offset(conn, (uint64_t)n);
+ return 0;
+}
+
+static int h3_acked_stream_data_cb(ngtcp2_conn *conn, int64_t stream_id,
+ uint64_t offset, uint64_t datalen,
+ void *user_data, void *stream_user_data)
+{
+ H3Conn *hc = user_data;
+ if (hc->h3conn)
+ nghttp3_conn_add_ack_offset(hc->h3conn, stream_id, datalen);
+ return 0;
+}
+
+static int h3_stream_close_cb(ngtcp2_conn *conn, uint32_t flags,
+ int64_t stream_id, uint64_t app_error_code,
+ void *user_data, void *stream_user_data)
+{
+ H3Conn *hc = user_data;
+ if (hc->h3conn) {
+ if (!app_error_code)
+ app_error_code = NGHTTP3_H3_NO_ERROR;
+ nghttp3_conn_close_stream(hc->h3conn, stream_id, app_error_code);
+ }
+ if (hc->cur && stream_id == hc->cur->stream_id)
+ hc->cur->stream_done = 1;
+ return 0;
+}
+
+static int h3_extend_max_stream_data_cb(ngtcp2_conn *conn, int64_t stream_id,
+ uint64_t max_data, void *user_data,
+ void *stream_user_data)
+{
+ H3Conn *hc = user_data;
+ if (hc->h3conn)
+ nghttp3_conn_unblock_stream(hc->h3conn, stream_id);
+ return 0;
+}
+
+/* ---- nghttp3 callbacks (user_data = H3Conn*) ---- */
+
+static int h3_http_recv_data_cb(nghttp3_conn *conn, int64_t stream_id,
+ const uint8_t *data, size_t datalen,
+ void *user_data, void *stream_user_data)
+{
+ H3Conn *hc = user_data;
+ HTTP3Context *c = hc->cur;
+ if (!c || stream_id != c->stream_id)
+ return 0;
+ return h3_buf_append(c, data, datalen) < 0 ? NGHTTP3_ERR_CALLBACK_FAILURE
: 0;
+}
+
+static int h3_recv_header_cb(nghttp3_conn *conn, int64_t stream_id, int32_t
token,
+ nghttp3_rcbuf *name, nghttp3_rcbuf *value,
uint8_t flags,
+ void *user_data, void *stream_user_data)
+{
+ H3Conn *hc = user_data;
+ HTTP3Context *c = hc->cur;
+ nghttp3_vec n, v;
+ char vb[2048];
+ size_t vn;
+
+ if (!c || stream_id != c->stream_id)
+ return 0;
+ n = nghttp3_rcbuf_get_buf(name);
+ v = nghttp3_rcbuf_get_buf(value);
+ vn = FFMIN(v.len, sizeof(vb) - 1);
+ memcpy(vb, v.base, vn);
+ vb[vn] = 0;
+
+ if (n.len == 7 && !av_strncasecmp((const char *)n.base, ":status", 7))
+ c->status = atoi(vb);
+ else if (n.len == 8 && !av_strncasecmp((const char *)n.base, "location",
8))
+ av_strlcpy(c->location, vb, sizeof(c->location));
+ else if (n.len == 13 && !av_strncasecmp((const char *)n.base,
"content-range", 13)) {
+ char *slash = strchr(vb, '/');
+ if (slash && slash[1] && slash[1] != '*')
+ c->filesize = strtoll(slash + 1, NULL, 10);
+ } else if (n.len == 14 && !av_strncasecmp((const char *)n.base,
"content-length", 14)) {
+ if (c->status == 200)
+ c->filesize = strtoll(vb, NULL, 10);
+ }
+ return 0;
+}
+
+static int h3_end_headers_cb(nghttp3_conn *conn, int64_t stream_id, int fin,
+ void *user_data, void *stream_user_data)
+{
+ H3Conn *hc = user_data;
+ if (hc->cur && stream_id == hc->cur->stream_id)
+ hc->cur->headers_done = 1;
+ return 0;
+}
+
+/* ---- connection bring-up ---- */
+
+static int h3_init_tls(URLContext *h, H3Conn *hc, const char *host)
+{
+ hc->conn_ref.get_conn = h3_get_conn;
+ hc->conn_ref.user_data = hc;
+
+#if CONFIG_GNUTLS
+ {
+ int rv;
+ /* TLS 1.3 only, QUIC-compatible cipher/group set */
+ static const char priority[] =
+ "%DISABLE_TLS13_COMPAT_MODE:NORMAL:-VERS-ALL:+VERS-TLS1.3:"
+
"-CIPHER-ALL:+AES-128-GCM:+AES-256-GCM:+CHACHA20-POLY1305:+AES-128-CCM:"
+ "-GROUP-ALL:+GROUP-SECP256R1:+GROUP-SECP384R1:+GROUP-SECP521R1:"
+ "+GROUP-X25519:+GROUP-X448";
+ gnutls_datum_t alpn = { (unsigned char *)H3_ALPN, sizeof(H3_ALPN) - 1
};
+
+ if (gnutls_certificate_allocate_credentials(&hc->cred) != 0)
+ return AVERROR_EXTERNAL;
+ gnutls_certificate_set_x509_system_trust(hc->cred);
+
+ if (gnutls_init(&hc->session, GNUTLS_CLIENT) != 0)
+ return AVERROR_EXTERNAL;
+ if ((rv = gnutls_priority_set_direct(hc->session, priority, NULL)) !=
0) {
+ av_log(h, AV_LOG_ERROR, "gnutls priority: %s\n",
gnutls_strerror(rv));
+ return AVERROR_EXTERNAL;
+ }
+ if (ngtcp2_crypto_gnutls_configure_client_session(hc->session) != 0)
+ return AVERROR_EXTERNAL;
+
+ gnutls_session_set_ptr(hc->session, &hc->conn_ref);
+ if (gnutls_credentials_set(hc->session, GNUTLS_CRD_CERTIFICATE,
hc->cred) != 0)
+ return AVERROR_EXTERNAL;
+ gnutls_alpn_set_protocols(hc->session, &alpn, 1,
GNUTLS_ALPN_MANDATORY);
+ gnutls_server_name_set(hc->session, GNUTLS_NAME_DNS, host,
strlen(host)); /* SNI */
+ /* verify the server cert chain + match the hostname; the handshake
+ fails on an invalid/mismatched cert. */
+ gnutls_session_set_verify_cert(hc->session, host, 0);
+ }
+#else
+ {
+ static const uint8_t alpn[] = { 2, 'h', '3' }; /* wire format: len +
"h3" */
+
+ hc->ssl_ctx = SSL_CTX_new(TLS_client_method());
+ if (!hc->ssl_ctx)
+ return AVERROR_EXTERNAL;
+#if defined(OPENSSL_IS_BORINGSSL)
+ if (ngtcp2_crypto_boringssl_configure_client_context(hc->ssl_ctx) != 0)
+ return AVERROR_EXTERNAL;
+#endif
+ SSL_CTX_set_default_verify_paths(hc->ssl_ctx); /* system trust store */
+#if !defined(OPENSSL_IS_BORINGSSL)
+ /* OpenSSL 3.5 has no ngtcp2 *context* configure (unlike BoringSSL);
+ set the QUIC-compatible TLS 1.3 ciphersuites + key-exchange groups
+ the ossl helper expects, or the handshake fails with ERR_CRYPTO. */
+ if (SSL_CTX_set_ciphersuites(hc->ssl_ctx,
+ "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:"
+ "TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256") != 1 ||
+ SSL_CTX_set1_groups_list(hc->ssl_ctx,
+ "X25519:P-256:P-384:P-521:X25519MLKEM768") != 1)
+ return AVERROR_EXTERNAL;
+#endif
+
+ hc->ssl = SSL_new(hc->ssl_ctx);
+ if (!hc->ssl)
+ return AVERROR_EXTERNAL;
+
+#if !defined(OPENSSL_IS_BORINGSSL)
+ /* OpenSSL 3.5 native QUIC: per-session crypto ctx bound to the SSL */
+ ff_thread_once(&h3_ossl_once, h3_ossl_global_init);
+ if (ngtcp2_crypto_ossl_ctx_new(&hc->ossl_ctx, NULL) != 0)
+ return AVERROR_EXTERNAL;
+ ngtcp2_crypto_ossl_ctx_set_ssl(hc->ossl_ctx, hc->ssl);
+ if (ngtcp2_crypto_ossl_configure_client_session(hc->ssl) != 0)
+ return AVERROR_EXTERNAL;
+#endif
+
+ SSL_set_app_data(hc->ssl, &hc->conn_ref); /* crypto helper finds the
conn here */
+ SSL_set_connect_state(hc->ssl);
+ SSL_set_alpn_protos(hc->ssl, alpn, sizeof(alpn));
+ SSL_set_tlsext_host_name(hc->ssl, host); /* SNI */
+ /* verify the server cert chain + match the hostname; the handshake
+ fails on an invalid/mismatched cert. */
+ SSL_set_verify(hc->ssl, SSL_VERIFY_PEER, NULL);
+ SSL_set1_host(hc->ssl, host);
+ }
+#endif
+ return 0;
+}
+
+static int h3_connect_udp(URLContext *h, H3Conn *hc, const char *host, const
char *port)
+{
+ struct addrinfo hints = { 0 }, *res = NULL, *ai;
+ int fd = -1, rv;
+
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ if ((rv = getaddrinfo(host, port, &hints, &res)) != 0) {
+ av_log(h, AV_LOG_ERROR, "getaddrinfo(%s:%s): %s\n", host, port,
gai_strerror(rv));
+ return AVERROR(EIO);
+ }
+ for (ai = res; ai; ai = ai->ai_next) {
+ fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ if (fd < 0)
+ continue;
+ if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) {
+ memcpy(&hc->remote_addr, ai->ai_addr, ai->ai_addrlen);
+ hc->remote_addrlen = ai->ai_addrlen;
+ break;
+ }
+ close(fd);
+ fd = -1;
+ }
+ freeaddrinfo(res);
+ if (fd < 0)
+ return AVERROR(EIO);
+ hc->local_addrlen = sizeof(hc->local_addr);
+ getsockname(fd, (struct sockaddr *)&hc->local_addr, &hc->local_addrlen);
+ hc->fd = fd;
+ return 0;
+}
+
+static int h3_init_quic(URLContext *h, H3Conn *hc)
+{
+ ngtcp2_settings settings;
+ ngtcp2_transport_params params;
+ ngtcp2_cid scid, dcid;
+ ngtcp2_path path;
+ int rv;
+
+ static const ngtcp2_callbacks callbacks = {
+ .client_initial = ngtcp2_crypto_client_initial_cb,
+ .recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb,
+ .encrypt = ngtcp2_crypto_encrypt_cb,
+ .decrypt = ngtcp2_crypto_decrypt_cb,
+ .hp_mask = ngtcp2_crypto_hp_mask_cb,
+ .recv_retry = ngtcp2_crypto_recv_retry_cb,
+ .update_key = ngtcp2_crypto_update_key_cb,
+ .delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb,
+ .delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb,
+ .get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb,
+ .version_negotiation = ngtcp2_crypto_version_negotiation_cb,
+ .rand = h3_rand_cb,
+ .get_new_connection_id = h3_get_new_cid_cb,
+ .recv_stream_data = h3_recv_stream_data_cb,
+ .acked_stream_data_offset = h3_acked_stream_data_cb,
+ .stream_close = h3_stream_close_cb,
+ .extend_max_stream_data = h3_extend_max_stream_data_cb,
+ };
+
+ h3_random(hc->sr_secret, sizeof(hc->sr_secret));
+ scid.datalen = 17;
+ h3_random(scid.data, scid.datalen);
+ dcid.datalen = 18;
+ h3_random(dcid.data, dcid.datalen);
+
+ ngtcp2_settings_default(&settings);
+ settings.initial_ts = h3_timestamp();
+
+ ngtcp2_transport_params_default(¶ms);
+ params.initial_max_streams_uni = 3;
+ params.initial_max_stream_data_bidi_local = 1024 * 1024;
+ params.initial_max_stream_data_uni = 256 * 1024;
+ params.initial_max_data = 8 * 1024 * 1024;
+ params.max_idle_timeout = 30 * NGTCP2_SECONDS;
+ params.active_connection_id_limit = 7;
+
+ path.local.addr = (struct sockaddr *)&hc->local_addr;
+ path.local.addrlen = hc->local_addrlen;
+ path.remote.addr = (struct sockaddr *)&hc->remote_addr;
+ path.remote.addrlen = hc->remote_addrlen;
+ path.user_data = NULL;
+
+ rv = ngtcp2_conn_client_new(&hc->conn, &dcid, &scid, &path,
+ NGTCP2_PROTO_VER_V1, &callbacks,
+ &settings, ¶ms, NULL, hc);
+ if (rv != 0) {
+ av_log(h, AV_LOG_ERROR, "ngtcp2_conn_client_new: %s\n",
ngtcp2_strerror(rv));
+ return AVERROR_EXTERNAL;
+ }
+ ngtcp2_conn_set_tls_native_handle(hc->conn, H3_TLS_HANDLE(hc));
+ /* let ngtcp2 emit keep-alive packets once idle 15s; the pool reaper pumps
+ parked connections so these actually go out and the conn survives the
+ 30s idle timeout for reuse by a later request. */
+ ngtcp2_conn_set_keep_alive_timeout(hc->conn, 15 * NGTCP2_SECONDS);
+ return 0;
+}
+
+static int h3_setup_http3(URLContext *h, H3Conn *hc)
+{
+ nghttp3_settings settings;
+ int64_t ctrl_id, enc_id, dec_id;
+ int rv;
+ static const nghttp3_callbacks callbacks = {
+ .recv_data = h3_http_recv_data_cb,
+ .recv_header = h3_recv_header_cb,
+ .end_headers = h3_end_headers_cb,
+ .stream_close = NULL,
+ };
+
+ nghttp3_settings_default(&settings);
+ settings.qpack_max_dtable_capacity = 4096;
+ settings.qpack_blocked_streams = 100;
+
+ rv = nghttp3_conn_client_new(&hc->h3conn, &callbacks, &settings,
+ nghttp3_mem_default(), hc);
+ if (rv != 0) {
+ av_log(h, AV_LOG_ERROR, "nghttp3_conn_client_new: %s\n",
nghttp3_strerror(rv));
+ return AVERROR_EXTERNAL;
+ }
+ if (ngtcp2_conn_open_uni_stream(hc->conn, &ctrl_id, NULL) != 0 ||
+ nghttp3_conn_bind_control_stream(hc->h3conn, ctrl_id) != 0)
+ return AVERROR_EXTERNAL;
+ if (ngtcp2_conn_open_uni_stream(hc->conn, &enc_id, NULL) != 0 ||
+ ngtcp2_conn_open_uni_stream(hc->conn, &dec_id, NULL) != 0 ||
+ nghttp3_conn_bind_qpack_streams(hc->h3conn, enc_id, dec_id) != 0)
+ return AVERROR_EXTERNAL;
+ return 0;
+}
+
+/* ---- I/O pump (operates on H3Conn) ---- */
+
+static int h3_write(URLContext *h, H3Conn *hc)
+{
+ uint8_t buf[1452];
+ ngtcp2_path_storage ps;
+ ngtcp2_pkt_info pi;
+ nghttp3_vec vec[16];
+
+ ngtcp2_path_storage_zero(&ps);
+ for (;;) {
+ int64_t stream_id = -1;
+ int fin = 0;
+ nghttp3_ssize sveccnt = 0;
+ ngtcp2_ssize ndatalen, nwrite;
+ uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE;
+
+ if (hc->h3conn) {
+ sveccnt = nghttp3_conn_writev_stream(hc->h3conn, &stream_id, &fin,
+ vec, FF_ARRAY_ELEMS(vec));
+ if (sveccnt < 0)
+ return AVERROR_EXTERNAL;
+ }
+ if (fin)
+ flags |= NGTCP2_WRITE_STREAM_FLAG_FIN;
+
+ nwrite = ngtcp2_conn_writev_stream(hc->conn, &ps.path, &pi, buf,
sizeof(buf),
+ &ndatalen, flags, stream_id,
+ (const ngtcp2_vec *)vec,
+ (size_t)sveccnt, h3_timestamp());
+ if (nwrite < 0) {
+ if (nwrite == NGTCP2_ERR_STREAM_DATA_BLOCKED) {
+ nghttp3_conn_block_stream(hc->h3conn, stream_id);
+ continue;
+ }
+ if (nwrite == NGTCP2_ERR_STREAM_SHUT_WR) {
+ nghttp3_conn_shutdown_stream_write(hc->h3conn, stream_id);
+ continue;
+ }
+ if (nwrite == NGTCP2_ERR_WRITE_MORE) {
+ nghttp3_conn_add_write_offset(hc->h3conn, stream_id, ndatalen);
+ continue;
+ }
+ av_log(h, AV_LOG_ERROR, "writev_stream: %s\n",
ngtcp2_strerror((int)nwrite));
+ return AVERROR_EXTERNAL;
+ }
+ if (ndatalen >= 0)
+ nghttp3_conn_add_write_offset(hc->h3conn, stream_id, ndatalen);
+ if (nwrite == 0)
+ return 0;
+ if (send(hc->fd, buf, nwrite, 0) < 0)
+ return AVERROR(EIO);
+ }
+}
+
+static int h3_read_socket(URLContext *h, H3Conn *hc)
+{
+ uint8_t buf[H3_DGRAM];
+ ngtcp2_path path;
+ ngtcp2_pkt_info pi = { 0 };
+ ssize_t nread;
+ int rv;
+
+ nread = recv(hc->fd, buf, sizeof(buf), 0);
+ if (nread < 0)
+ return AVERROR(EIO);
+
+ path.local.addr = (struct sockaddr *)&hc->local_addr;
+ path.local.addrlen = hc->local_addrlen;
+ path.remote.addr = (struct sockaddr *)&hc->remote_addr;
+ path.remote.addrlen = hc->remote_addrlen;
+ path.user_data = NULL;
+
+ rv = ngtcp2_conn_read_pkt(hc->conn, &path, &pi, buf, nread,
h3_timestamp());
+ if (rv != 0) {
+ const ngtcp2_ccerr *e = ngtcp2_conn_get_ccerr(hc->conn);
+ av_log(h, AV_LOG_ERROR, "read_pkt: %s; ccerr 0x%"PRIx64" %.*s\n",
+ ngtcp2_strerror(rv), e ? (uint64_t)e->error_code : 0,
+ e ? (int)e->reasonlen : 0, e ? (const char *)e->reason : "");
+ return AVERROR_EXTERNAL;
+ }
+ return 0;
+}
+
+static int h3_pump(URLContext *h, H3Conn *hc, int timeout_ms)
+{
+ struct pollfd pfd = { .fd = hc->fd, .events = POLLIN };
+ ngtcp2_tstamp expiry;
+ uint64_t now;
+ int pr, d;
+
+ if (h3_write(h, hc) < 0)
+ return AVERROR_EXTERNAL;
+
+ expiry = ngtcp2_conn_get_expiry(hc->conn);
+ now = h3_timestamp();
+ if (expiry != UINT64_MAX) {
+ d = (int)(expiry > now ? (expiry - now) / 1000000 : 0);
+ if (d < timeout_ms)
+ timeout_ms = d;
+ }
+
+ pr = poll(&pfd, 1, timeout_ms);
+ if (pr < 0)
+ return AVERROR(EIO);
+ if (pr > 0 && (pfd.revents & POLLIN)) {
+ int rv = h3_read_socket(h, hc);
+ if (rv < 0)
+ return rv;
+ } else if (ngtcp2_conn_handle_expiry(hc->conn, h3_timestamp()) != 0) {
+ return AVERROR_EXTERNAL;
+ }
+ return h3_write(h, hc);
+}
+
+static int h3_handshake(URLContext *h, H3Conn *hc, int64_t timeout_us)
+{
+ int64_t deadline = av_gettime_relative() + timeout_us;
+ while (!ngtcp2_conn_get_handshake_completed(hc->conn)) {
+ int rv;
+ if (av_gettime_relative() > deadline)
+ return AVERROR(ETIMEDOUT);
+ if ((rv = h3_pump(h, hc, 1000)) < 0)
+ return rv;
+ }
+ return 0;
+}
+
+static int h3_conn_alive(H3Conn *hc)
+{
+ return hc->conn &&
+ ngtcp2_conn_get_handshake_completed(hc->conn) &&
+ !ngtcp2_conn_in_closing_period2(hc->conn) &&
+ !ngtcp2_conn_in_draining_period2(hc->conn);
+}
+
+static H3Conn *h3conn_alloc(void)
+{
+ H3Conn *hc = av_mallocz(sizeof(*hc));
+ if (hc)
+ hc->fd = -1;
+ return hc;
+}
+
+static void h3conn_free(H3Conn *hc)
+{
+ if (!hc)
+ return;
+ if (hc->h3conn) nghttp3_conn_del(hc->h3conn);
+ if (hc->conn) ngtcp2_conn_del(hc->conn);
+#if CONFIG_GNUTLS
+ if (hc->session) gnutls_deinit(hc->session);
+ if (hc->cred) gnutls_certificate_free_credentials(hc->cred);
+#else
+#if !defined(OPENSSL_IS_BORINGSSL)
+ if (hc->ossl_ctx) ngtcp2_crypto_ossl_ctx_del(hc->ossl_ctx);
+#endif
+ if (hc->ssl) SSL_free(hc->ssl);
+ if (hc->ssl_ctx) SSL_CTX_free(hc->ssl_ctx);
+#endif
+ if (hc->fd >= 0) close(hc->fd);
+ av_free(hc);
+}
+
+static int h3_dial(URLContext *h, H3Conn *hc, const char *portstr, int64_t
timeout_us)
+{
+ int ret;
+ if ((ret = h3_connect_udp(h, hc, hc->host, portstr)) < 0) return ret;
+ if ((ret = h3_init_tls(h, hc, hc->host)) < 0) return ret;
+ if ((ret = h3_init_quic(h, hc)) < 0) return ret;
+ if ((ret = h3_handshake(h, hc, timeout_us)) < 0) return ret;
+ if ((ret = h3_setup_http3(h, hc)) < 0) return ret;
+ return 0;
+}
+
+/* ---- pool ---- */
+
+/* Background reaper: pumps parked connections so ngtcp2 sends keep-alive PINGs
+ (keeping them alive past the idle timeout for reuse) and evicts dead ones.
*/
+static void *h3_reaper(void *arg)
+{
+ for (;;) {
+ int i;
+ av_usleep(3 * 1000000);
+ ff_mutex_lock(&h3_pool_mutex);
+ for (i = 0; i < H3_POOL_MAX; i++) {
+ H3Conn *hc = h3_pool[i];
+ if (!hc)
+ continue;
+ if (h3_pump(NULL, hc, 0) < 0 || !h3_conn_alive(hc)) {
+ h3conn_free(hc);
+ h3_pool[i] = NULL;
+ }
+ }
+ ff_mutex_unlock(&h3_pool_mutex);
+ }
+ return NULL;
+}
+
+static void h3_start_reaper(void)
+{
+ pthread_t t;
+ if (pthread_create(&t, NULL, h3_reaper, NULL) == 0)
+ pthread_detach(t);
+}
+
+static H3Conn *h3_pool_take(const char *host, int port)
+{
+ H3Conn *hc = NULL;
+ int i;
+ ff_mutex_lock(&h3_pool_mutex);
+ for (i = 0; i < H3_POOL_MAX; i++) {
+ if (h3_pool[i] && h3_pool[i]->port == port &&
+ !strcmp(h3_pool[i]->host, host)) {
+ hc = h3_pool[i];
+ h3_pool[i] = NULL;
+ break;
+ }
+ }
+ ff_mutex_unlock(&h3_pool_mutex);
+ return hc;
+}
+
+static void h3_pool_put(H3Conn *hc)
+{
+ static AVOnce reaper_once = AV_ONCE_INIT;
+ H3Conn *evict = NULL;
+ int i;
+ ff_thread_once(&reaper_once, h3_start_reaper);
+ ff_mutex_lock(&h3_pool_mutex);
+ for (i = 0; i < H3_POOL_MAX; i++) {
+ if (!h3_pool[i]) {
+ h3_pool[i] = hc;
+ hc = NULL;
+ break;
+ }
+ }
+ if (hc) {
+ /* full: evict the oldest (slot 0), shift down, append the new one */
+ evict = h3_pool[0];
+ for (i = 1; i < H3_POOL_MAX; i++)
+ h3_pool[i - 1] = h3_pool[i];
+ h3_pool[H3_POOL_MAX - 1] = hc;
+ }
+ ff_mutex_unlock(&h3_pool_mutex);
+ h3conn_free(evict);
+}
+
+/* ---- request ---- */
+
+static int h3_start_request(URLContext *h, HTTP3Context *c, int64_t
range_start)
+{
+ H3Conn *hc = c->hc;
+ int rv;
+ char rangebuf[64];
+ size_t nvlen;
+#define MK_NV(N, V) { (uint8_t *)(N), (uint8_t *)(V), sizeof(N) - 1,
strlen(V), NGHTTP3_NV_FLAG_NONE }
+ nghttp3_nv nva[6] = {
+ MK_NV(":method", "GET"),
+ MK_NV(":scheme", "https"),
+ { (uint8_t *)":authority", (uint8_t *)hc->host, sizeof(":authority") -
1, strlen(hc->host), NGHTTP3_NV_FLAG_NONE },
+ { (uint8_t *)":path", (uint8_t *)c->path, sizeof(":path") - 1,
strlen(c->path), NGHTTP3_NV_FLAG_NONE },
+ MK_NV("user-agent", "ffmpeg-http3/0.1"),
+ };
+ nvlen = 5;
+
+ hc->cur = c;
+ if (c->stream_id >= 0 && !c->stream_done)
+ ngtcp2_conn_shutdown_stream(hc->conn, 0, c->stream_id,
NGHTTP3_H3_REQUEST_CANCELLED);
+
+ c->rb_len = c->rb_off = 0;
+ c->stream_done = c->headers_done = c->status = 0;
+
+ if (range_start > 0) {
+ snprintf(rangebuf, sizeof(rangebuf), "bytes=%"PRId64"-", range_start);
+ nva[nvlen].name = (uint8_t *)"range"; nva[nvlen].namelen = 5;
+ nva[nvlen].value = (uint8_t *)rangebuf; nva[nvlen].valuelen =
strlen(rangebuf);
+ nva[nvlen].flags = NGHTTP3_NV_FLAG_NONE;
+ nvlen++;
+ }
+
+ if (ngtcp2_conn_open_bidi_stream(hc->conn, &c->stream_id, NULL) != 0)
+ return AVERROR_EXTERNAL;
+ rv = nghttp3_conn_submit_request(hc->h3conn, c->stream_id, nva, nvlen,
NULL, hc);
+ if (rv != 0) {
+ av_log(h, AV_LOG_ERROR, "submit_request: %s\n", nghttp3_strerror(rv));
+ return AVERROR_EXTERNAL;
+ }
+ return 0;
+}
+
+static int h3_await_headers(URLContext *h, HTTP3Context *c)
+{
+ int64_t deadline = av_gettime_relative() + c->open_timeout_us;
+ while (!c->headers_done && !c->stream_done) {
+ int rv;
+ if (av_gettime_relative() > deadline)
+ return AVERROR(ETIMEDOUT);
+ if ((rv = h3_pump(h, c->hc, 1000)) < 0)
+ return rv;
+ }
+ return 0;
+}
+
+static int h3_status_error(int s)
+{
+ switch (s) {
+ case 400: return AVERROR_HTTP_BAD_REQUEST;
+ case 401: return AVERROR_HTTP_UNAUTHORIZED;
+ case 403: return AVERROR_HTTP_FORBIDDEN;
+ case 404: return AVERROR_HTTP_NOT_FOUND;
+ case 429: return AVERROR_HTTP_TOO_MANY_REQUESTS;
+ default: return s >= 500 ? AVERROR_HTTP_SERVER_ERROR :
AVERROR_HTTP_OTHER_4XX;
+ }
+}
+
+/* ---- URLProtocol ---- */
+
+/* Alt-Svc discovery: a plain TLS-over-TCP HTTP/1.1 HEAD to learn whether the
+ origin advertises HTTP/3 and on which port (RFC 9114 §3.1.1). Returns the
+ advertised h3 port, or <0 if none. This is the "upgrade" path: instead of
+ blindly assuming h3 (prior knowledge), confirm + discover the port first. */
+static int h3_altsvc_probe(URLContext *h, const char *host, int port)
+{
+ struct addrinfo hints = { 0 }, *res = NULL, *ai;
+#if CONFIG_GNUTLS
+ gnutls_session_t s = NULL;
+ gnutls_certificate_credentials_t cred = NULL;
+#else
+ SSL_CTX *ctx = NULL;
+ SSL *ssl = NULL;
+ static const uint8_t alpn[] = { 8, 'h','t','t','p','/','1','.','1' };
+#endif
+ char portstr[12], req[512], buf[8192];
+ int fd = -1, ret = AVERROR(EIO), n, off = 0;
+ char *as, *eol, *h3, *v, *end, *colon;
+
+ snprintf(portstr, sizeof(portstr), "%d", port);
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ if (getaddrinfo(host, portstr, &hints, &res) != 0)
+ return AVERROR(EIO);
+ for (ai = res; ai; ai = ai->ai_next) {
+ fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ if (fd < 0) continue;
+ if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
+ close(fd); fd = -1;
+ }
+ freeaddrinfo(res);
+ if (fd < 0)
+ return AVERROR(EIO);
+
+#if CONFIG_GNUTLS
+ if (gnutls_certificate_allocate_credentials(&cred) != 0) goto out;
+ gnutls_certificate_set_x509_system_trust(cred);
+ if (gnutls_init(&s, GNUTLS_CLIENT) != 0) goto out;
+ gnutls_set_default_priority(s);
+ gnutls_credentials_set(s, GNUTLS_CRD_CERTIFICATE, cred);
+ gnutls_server_name_set(s, GNUTLS_NAME_DNS, host, strlen(host));
+ gnutls_session_set_verify_cert(s, host, 0);
+ gnutls_transport_set_int(s, fd);
+ gnutls_handshake_set_timeout(s, 5000);
+ do { n = gnutls_handshake(s); } while (n < 0 && !gnutls_error_is_fatal(n));
+ if (n < 0) {
+ av_log(h, AV_LOG_WARNING, "alt-svc probe TLS: %s\n",
gnutls_strerror(n));
+ goto out;
+ }
+#else
+ ctx = SSL_CTX_new(TLS_client_method());
+ if (!ctx) goto out;
+ SSL_CTX_set_default_verify_paths(ctx);
+ ssl = SSL_new(ctx);
+ if (!ssl) goto out;
+ SSL_set_fd(ssl, fd);
+ SSL_set_tlsext_host_name(ssl, host);
+ SSL_set1_host(ssl, host);
+ SSL_set_verify(ssl, SSL_VERIFY_PEER, NULL);
+ SSL_set_alpn_protos(ssl, alpn, sizeof(alpn));
+ if (SSL_connect(ssl) != 1) {
+ av_log(h, AV_LOG_WARNING, "alt-svc probe TLS handshake failed\n");
+ goto out;
+ }
+#endif
+
+ snprintf(req, sizeof(req),
+ "HEAD / HTTP/1.1\r\nHost: %s\r\nUser-Agent:
ffmpeg-http3/0.1\r\nConnection: close\r\n\r\n",
+ host);
+#if CONFIG_GNUTLS
+ gnutls_record_send(s, req, strlen(req));
+ while (off < (int)sizeof(buf) - 1) {
+ n = gnutls_record_recv(s, buf + off, sizeof(buf) - 1 - off);
+ if (n == GNUTLS_E_AGAIN || n == GNUTLS_E_INTERRUPTED) continue;
+ if (n <= 0) break;
+ off += n;
+ buf[off] = 0;
+ if (strstr(buf, "\r\n\r\n")) break; /* headers complete */
+ }
+#else
+ SSL_write(ssl, req, strlen(req));
+ while (off < (int)sizeof(buf) - 1) {
+ n = SSL_read(ssl, buf + off, sizeof(buf) - 1 - off);
+ if (n <= 0) break;
+ off += n;
+ buf[off] = 0;
+ if (strstr(buf, "\r\n\r\n")) break; /* headers complete */
+ }
+#endif
+ buf[off] = 0;
+
+ as = av_stristr(buf, "alt-svc:");
+ if (!as) { ret = AVERROR(EPROTONOSUPPORT); goto out; }
+ if ((eol = strstr(as, "\r\n"))) *eol = 0;
+ h3 = av_stristr(as, "h3=");
+ if (!h3) { ret = AVERROR(EPROTONOSUPPORT); goto out; }
+ v = h3 + 3;
+ if (*v == '"') v++;
+ end = v;
+ while (*end && *end != '"' && *end != ';' && *end != ',') end++;
+ *end = 0;
+ colon = strrchr(v, ':');
+ ret = colon ? atoi(colon + 1) : port;
+ if (ret <= 0) ret = port;
+ av_log(h, AV_LOG_INFO, "http3: Alt-Svc advertises h3 on port %d\n", ret);
+
+out:
+#if CONFIG_GNUTLS
+ if (s) { gnutls_bye(s, GNUTLS_SHUT_WR); gnutls_deinit(s); }
+ if (cred) gnutls_certificate_free_credentials(cred);
+#else
+ if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
+ if (ctx) SSL_CTX_free(ctx);
+#endif
+ if (fd >= 0) close(fd);
+ return ret;
+}
+
+static int http3_open(URLContext *h, const char *uri, int flags)
+{
+ HTTP3Context *c = h->priv_data;
+ int port, ret, redirects = 0, reused;
+ char portstr[12], nexturi[4096], host[1024];
+
+ c->stream_id = -1;
+ c->off = 0;
+ c->open_timeout_us = 15 * 1000000;
+
+ av_strlcpy(nexturi, uri, sizeof(nexturi));
+ for (;;) {
+ port = -1;
+ c->filesize = -1;
+ av_url_split(NULL, 0, NULL, 0, host, sizeof(host), &port,
+ c->path, sizeof(c->path), nexturi);
+ if (port < 0)
+ port = 443;
+ if (!c->path[0])
+ av_strlcpy(c->path, "/", sizeof(c->path));
+ snprintf(portstr, sizeof(portstr), "%d", port);
+
+ if (c->altsvc) {
+ int p3 = h3_altsvc_probe(h, host, port);
+ if (p3 < 0) {
+ av_log(h, AV_LOG_ERROR, "http3: %s:%d does not advertise
HTTP/3\n", host, port);
+ ret = p3;
+ goto fail;
+ }
+ if (p3 != port) {
+ port = p3;
+ snprintf(portstr, sizeof(portstr), "%d", port);
+ }
+ }
+
+ reused = 0;
+ c->hc = h3_pool_take(host, port);
+ if (c->hc) {
+ c->hc->cur = c;
+ /* liveness: process any pending packets, then validate */
+ if (h3_pump(h, c->hc, 0) == 0 && h3_conn_alive(c->hc)) {
+ reused = 1;
+ av_log(h, AV_LOG_VERBOSE, "http3: reusing pooled connection to
%s:%d\n", host, port);
+ } else {
+ h3conn_free(c->hc);
+ c->hc = NULL;
+ }
+ }
+ if (!c->hc) {
+ c->hc = h3conn_alloc();
+ if (!c->hc) { ret = AVERROR(ENOMEM); goto fail; }
+ c->hc->cur = c;
+ av_strlcpy(c->hc->host, host, sizeof(c->hc->host));
+ c->hc->port = port;
+ if ((ret = h3_dial(h, c->hc, portstr, c->open_timeout_us)) < 0)
+ goto fail;
+ }
+
+ if ((ret = h3_start_request(h, c, 0)) < 0) goto fail;
+ if ((ret = h3_await_headers(h, c)) < 0) goto fail;
+
+ if (c->status >= 300 && c->status < 400 && c->location[0]) {
+ if (++redirects > 8) { ret = AVERROR(ELOOP); goto fail; }
+ av_log(h, AV_LOG_VERBOSE, "http3: %d redirect -> %s\n", c->status,
c->location);
+ if (av_strstart(c->location, "http", NULL))
+ av_strlcpy(nexturi, c->location, sizeof(nexturi));
+ else
+ snprintf(nexturi, sizeof(nexturi), "http3://%s:%d%s", host,
port, c->location);
+ h3conn_free(c->hc); /* don't pool a redirect source */
+ c->hc = NULL;
+ c->location[0] = 0;
+ continue;
+ }
+ if (c->status >= 400) { ret = h3_status_error(c->status); goto fail; }
+ break; /* 2xx */
+ }
+
+ av_log(h, AV_LOG_INFO, "http3: GET https://%s%s -> %d over HTTP/3%s\n",
+ host, c->path, c->status, reused ? " (reused conn)" : "");
+ return 0;
+fail:
+ h3conn_free(c->hc);
+ c->hc = NULL;
+ return ret;
+}
+
+static int http3_read(URLContext *h, unsigned char *buf, int size)
+{
+ HTTP3Context *c = h->priv_data;
+ int64_t deadline = av_gettime_relative() + c->open_timeout_us;
+
+ while (c->rb_off >= c->rb_len) {
+ int rv;
+ if (c->stream_done)
+ return AVERROR_EOF;
+ if (av_gettime_relative() > deadline)
+ return AVERROR(ETIMEDOUT);
+ if ((rv = h3_pump(h, c->hc, 1000)) < 0)
+ return rv;
+ }
+ {
+ int n = (int)FFMIN((size_t)size, c->rb_len - c->rb_off);
+ memcpy(buf, c->rb + c->rb_off, n);
+ c->rb_off += n;
+ c->off += n;
+ if (c->rb_off >= c->rb_len)
+ c->rb_off = c->rb_len = 0;
+ return n;
+ }
+}
+
+static int64_t http3_seek(URLContext *h, int64_t pos, int whence)
+{
+ HTTP3Context *c = h->priv_data;
+ int64_t newpos;
+ int ret;
+
+ if (whence == AVSEEK_SIZE)
+ return c->filesize >= 0 ? c->filesize : AVERROR(ENOSYS);
+ if (whence == SEEK_CUR)
+ newpos = c->off + pos;
+ else if (whence == SEEK_END) {
+ if (c->filesize < 0)
+ return AVERROR(ENOSYS);
+ newpos = c->filesize + pos;
+ } else
+ newpos = pos;
+ if (newpos < 0)
+ return AVERROR(EINVAL);
+ if (newpos == c->off)
+ return c->off;
+ if ((ret = h3_start_request(h, c, newpos)) < 0)
+ return ret;
+ c->off = newpos;
+ return newpos;
+}
+
+static int http3_close(URLContext *h)
+{
+ HTTP3Context *c = h->priv_data;
+ H3Conn *hc = c->hc;
+
+ if (hc) {
+ /* a clean, finished 2xx connection can be parked for reuse */
+ int poolable = c->stream_done && c->status >= 200 && c->status < 300 &&
+ h3_conn_alive(hc);
+ if (poolable) {
+ hc->cur = NULL;
+ h3_pump(h, hc, 0); /* flush any pending acks */
+ h3_pool_put(hc);
+ } else {
+ h3conn_free(hc);
+ }
+ c->hc = NULL;
+ }
+ av_freep(&c->rb);
+ return 0;
+}
+
+static const AVOption http3_options[] = {
+ { "altsvc", "discover HTTP/3 via the Alt-Svc header before connecting",
+ offsetof(HTTP3Context, altsvc), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1,
+ AV_OPT_FLAG_DECODING_PARAM },
+ { NULL }
+};
+
+static const AVClass http3_class = {
+ .class_name = "http3",
+ .item_name = av_default_item_name,
+ .option = http3_options,
+ .version = LIBAVUTIL_VERSION_INT,
+};
+
+const URLProtocol ff_http3_protocol = {
+ .name = "http3",
+ .url_open = http3_open,
+ .url_read = http3_read,
+ .url_seek = http3_seek,
+ .url_close = http3_close,
+ .priv_data_size = sizeof(HTTP3Context),
+ .priv_data_class = &http3_class,
+ .flags = URL_PROTOCOL_FLAG_NETWORK,
+};
diff --git a/libavformat/protocols.c b/libavformat/protocols.c
index 257b419..2b3022b 100644
--- a/libavformat/protocols.c
+++ b/libavformat/protocols.c
@@ -66,6 +66,7 @@ extern const URLProtocol ff_dtls_protocol;
extern const URLProtocol ff_udp_protocol;
extern const URLProtocol ff_udplite_protocol;
extern const URLProtocol ff_unix_protocol;
+extern const URLProtocol ff_http3_protocol;
extern const URLProtocol ff_libamqp_protocol;
extern const URLProtocol ff_librist_protocol;
extern const URLProtocol ff_librtmp_protocol;
--
2.47.3
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]