Commit 460cb05218 for ffmpeg
commit 460cb05218a236cb587516fe2c8b121403609d95
Author: Thomas Devoogdt <thomas@devoogdt.com>
Date: Sun Jul 19 19:24:11 2026 +0200
avformat/rtsp: send TEARDOWN on close despite a pending user interrupt
Closing an RTSP session while the AVFormatContext interrupt callback is
already asserted - e.g. ffplay terminated by SIGTERM or Ctrl-C - makes
every I/O on the control connection fail immediately, so the server never
sees a TEARDOWN. Its session stays alive until it times out (~10 s) and
an immediate reconnect is rejected with "453 Not Enough Bandwidth".
Open the control connection with an interrupt callback owned by
RTSPState. It forwards to the user callback, except while
rt->teardown_deadline is armed, in which case it only reports that
deadline. Every nested URLContext inherits a copy of the callback of the
context that opened it, so the bound applies to the whole transport
stack - tls -> tcp, tls -> httpproxy -> tcp and the HTTP(S) tunnelled
variants - without reaching into private contexts.
rtsp_read_close() arms that deadline (500 ms) and sends TEARDOWN
synchronously, so the 200 OK is consumed and the server releases the
session. The deadline stays armed for the connection shutdown that
follows, which keeps the total close time bounded.
While armed, ff_rtsp_read_reply_internal() also drains replies with a
mismatching CSeq. Keepalives are sent with ff_rtsp_send_cmd_async(), so
their replies may still be queued; without draining, the synchronous
TEARDOWN is satisfied by such a reply and its own 200 OK is left unread.
This is deliberately restricted to the close path: elsewhere a
mismatching CSeq keeps being accepted, as before, so no command can wait
unbounded on a non-conforming server.
Fixes: #23405
Signed-off-by: Thomas Devoogdt <thomas@devoogdt.com>
diff --git a/libavformat/rtsp.c b/libavformat/rtsp.c
index 51754f5d26..cdf7b7c389 100644
--- a/libavformat/rtsp.c
+++ b/libavformat/rtsp.c
@@ -1369,6 +1369,18 @@ start:
if (rt->seq != reply->seq) {
av_log(s, AV_LOG_WARNING, "CSeq %d expected, %d received.\n",
rt->seq, reply->seq);
+ /* At close time, drain stale async replies - e.g. queued keepalive
+ * OPTIONS or GET_PARAMETER responses - until the expected CSeq shows
+ * up. Otherwise ff_rtsp_send_cmd("TEARDOWN") in rtsp_read_close()
+ * is satisfied by a queued keepalive reply, leaving the TEARDOWN
+ * 200 OK unread so the server never frees the session. Only done
+ * while teardown_deadline bounds the wait; every other synchronous
+ * command keeps accepting a mismatching reply. */
+ if (rt->teardown_deadline) {
+ if (content_ptr)
+ av_freep(content_ptr);
+ goto start;
+ }
}
/* EOS */
@@ -1901,6 +1913,19 @@ static int rtsp_url_same_origin(const char *url1, const char *url2)
port1 == port2;
}
+static int rtsp_control_interrupt_cb(void *opaque)
+{
+ AVFormatContext *s = opaque;
+ RTSPState *rt = s->priv_data;
+
+ /* While closing, a pending user interrupt must not prevent TEARDOWN from
+ * being sent and its reply from being read; bound the wait instead. */
+ if (rt->teardown_deadline)
+ return av_gettime_relative() >= rt->teardown_deadline;
+
+ return ff_check_interrupt(&s->interrupt_callback);
+}
+
int ff_rtsp_connect(AVFormatContext *s)
{
RTSPState *rt = s->priv_data;
@@ -1917,6 +1942,8 @@ int ff_rtsp_connect(AVFormatContext *s)
socklen_t peer_len = sizeof(peer);
rt->stored_msg.expected_seq = -1;
+ rt->control_interrupt_cb.callback = rtsp_control_interrupt_cb;
+ rt->control_interrupt_cb.opaque = s;
if (rt->rtp_port_max < rt->rtp_port_min) {
av_log(s, AV_LOG_ERROR, "Invalid UDP port range, max port %d less "
"than min port %d\n", rt->rtp_port_max,
@@ -2008,7 +2035,7 @@ redirect:
/* GET requests */
if (ffurl_alloc(&rt->rtsp_hd, httpname, AVIO_FLAG_READ,
- &s->interrupt_callback) < 0) {
+ &rt->control_interrupt_cb) < 0) {
av_dict_free(&options);
err = AVERROR(EIO);
goto fail;
@@ -2050,7 +2077,7 @@ redirect:
/* POST requests */
if (ffurl_alloc(&rt->rtsp_hd_out, httpname, AVIO_FLAG_WRITE,
- &s->interrupt_callback) < 0 ) {
+ &rt->control_interrupt_cb) < 0 ) {
av_dict_free(&options);
err = AVERROR(EIO);
goto fail;
@@ -2111,7 +2138,7 @@ redirect:
host, port,
"?timeout=%"PRId64, rt->stimeout);
if ((ret = ffurl_open_whitelist(&rt->rtsp_hd, tcpname, AVIO_FLAG_READ_WRITE,
- &s->interrupt_callback, &proto_opts, s->protocol_whitelist, s->protocol_blacklist, NULL)) < 0) {
+ &rt->control_interrupt_cb, &proto_opts, s->protocol_whitelist, s->protocol_blacklist, NULL)) < 0) {
av_dict_free(&proto_opts);
err = ret;
goto fail;
diff --git a/libavformat/rtsp.h b/libavformat/rtsp.h
index 3c9c2c842d..a235fc81a7 100644
--- a/libavformat/rtsp.h
+++ b/libavformat/rtsp.h
@@ -357,6 +357,17 @@ typedef struct RTSPState {
* separately, eg for HTTP tunneling. */
URLContext *rtsp_hd_out;
+ /** Interrupt callback installed on the RTSP control connection(s). Every
+ * nested URLContext (tls, http, httpproxy, tcp) inherits a copy of it, so
+ * the whole transport stack consults teardown_deadline. */
+ AVIOInterruptCB control_interrupt_cb;
+
+ /** Monotonic deadline (av_gettime_relative() time base) bounding the
+ * close-time TEARDOWN exchange, or 0 while not closing. While armed, the
+ * control connection ignores the user interrupt callback and
+ * ff_rtsp_read_reply_internal() insists on the expected CSeq. */
+ int64_t teardown_deadline;
+
/** RTSP transport mode, such as plain or tunneled. */
enum RTSPControlTransport control_transport;
diff --git a/libavformat/rtspdec.c b/libavformat/rtspdec.c
index fada311367..0e592eebae 100644
--- a/libavformat/rtspdec.c
+++ b/libavformat/rtspdec.c
@@ -59,12 +59,26 @@ static const struct RTSPStatusMessage {
{ 0, "NULL" }
};
+/* Upper bound on the whole close-time TEARDOWN exchange. */
+#define RTSP_TEARDOWN_TIMEOUT (500 * 1000)
+
static int rtsp_read_close(AVFormatContext *s)
{
RTSPState *rt = s->priv_data;
- if (!(rt->rtsp_flags & RTSP_FLAG_LISTEN))
- ff_rtsp_send_cmd_async(s, "TEARDOWN", rt->control_uri, NULL);
+ if (!(rt->rtsp_flags & RTSP_FLAG_LISTEN)) {
+ /* Arm the deadline consulted by the control connection's interrupt
+ * callback, which every nested URLContext (tls, http, httpproxy, tcp)
+ * inherited. A pending user interrupt (Ctrl-C) is thus ignored on the
+ * whole transport stack until the deadline expires, so TEARDOWN goes
+ * out and its reply can be read. It stays armed for the connection
+ * shutdown below, which keeps the total close time bounded. The
+ * command is sent synchronously so the 200 OK is consumed and the
+ * server releases the session. */
+ rt->teardown_deadline = av_gettime_relative() + RTSP_TEARDOWN_TIMEOUT;
+ ff_rtsp_send_cmd(s, "TEARDOWN", rt->control_uri, NULL,
+ &(RTSPMessageHeader){0}, NULL);
+ }
ff_rtsp_close_streams(s);
ff_rtsp_close_connections(s);