PR #23406 opened by Thomas Devoogdt (ThomasDevoogdt)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23406
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23406.patch

When ffplay exits via SIGTERM/Ctrl-C, the AVFormatContext interrupt
callback is set.  The RTSP demuxer skipped TEARDOWN entirely, leaving
the server session alive (~10 s), which caused "453 Not Enough
Bandwidth" on immediate reconnect.

Fix the problem at every layer it can occur:

fftools/ffplay: make sigterm_handler() set a flag instead of calling
exit() directly.  The main event loop checks the flag and routes
through do_exit() -> avformat_close_input() so that TEARDOWN is
reached during normal shutdown.

avformat/rtspdec: in rtsp_read_close(), replace the user interrupt
callback with a 500 ms deadline callback on both the TLS URLContext
and its TCP child before sending TEARDOWN.  Without patching the TCP
child, GnuTLS/OpenSSL push callbacks receive AVERROR_EXIT from the
TCP write and previously returned 0 ("sent 0 bytes, try again"),
causing an infinite busy-spin.  Switch to ff_rtsp_send_cmd() (blocking)
instead of ff_rtsp_send_cmd_async() so the client waits for the
server's 200 OK before closing, guaranteeing the session is freed.

avformat/tls: add ff_tls_get_underlying() to expose the TCP/UDP child
URLContext of a TLS context through a proper API, avoiding direct
access to private priv_data.

avformat/tls_gnutls, tls_openssl: fix the AVERROR_EXIT -> 0 mapping
in the TLS push callbacks.  Returning 0 on interrupt signals "sent 0
bytes" to the TLS library and causes a busy-spin; use EINTR / hard
error instead.

Fixes: #23405

Signed-off-by: Thomas Devoogdt <[email protected]>

# Summary of changes

Briefly describe what this PR does and why.

<!--
If this PR requires new FATE test samples, attach them to the PR and
list their target paths below (relative to the fate-suite root).

Attached filenames must match the sample's filename:

```fate-samples
# e.g. vorbis/new-sample.ogg
```
-->



>From 236cba060fd6120ec804ace01df0b1cf7a19b138 Mon Sep 17 00:00:00 2001
From: Thomas Devoogdt <[email protected]>
Date: Mon, 8 Jun 2026 10:58:52 +0200
Subject: [PATCH] avformat/rtspdec: send TEARDOWN on close despite user
 interrupt

When ffplay exits via SIGTERM/Ctrl-C, the AVFormatContext interrupt
callback is set.  The RTSP demuxer skipped TEARDOWN entirely, leaving
the server session alive (~10 s), which caused "453 Not Enough
Bandwidth" on immediate reconnect.

Fix the problem at every layer it can occur:

fftools/ffplay: make sigterm_handler() set a flag instead of calling
exit() directly.  The main event loop checks the flag and routes
through do_exit() -> avformat_close_input() so that TEARDOWN is
reached during normal shutdown.

avformat/rtspdec: in rtsp_read_close(), replace the user interrupt
callback with a 500 ms deadline callback on both the TLS URLContext
and its TCP child before sending TEARDOWN.  Without patching the TCP
child, GnuTLS/OpenSSL push callbacks receive AVERROR_EXIT from the
TCP write and previously returned 0 ("sent 0 bytes, try again"),
causing an infinite busy-spin.  Switch to ff_rtsp_send_cmd() (blocking)
instead of ff_rtsp_send_cmd_async() so the client waits for the
server's 200 OK before closing, guaranteeing the session is freed.

avformat/tls: add ff_tls_get_underlying() to expose the TCP/UDP child
URLContext of a TLS context through a proper API, avoiding direct
access to private priv_data.

avformat/tls_gnutls, tls_openssl: fix the AVERROR_EXIT -> 0 mapping
in the TLS push callbacks.  Returning 0 on interrupt signals "sent 0
bytes" to the TLS library and causes a busy-spin; use EINTR / hard
error instead.

Fixes: #23405

Signed-off-by: Thomas Devoogdt <[email protected]>
---
 fftools/ffplay.c          |  6 +++++-
 libavformat/rtspdec.c     | 34 ++++++++++++++++++++++++++++++++--
 libavformat/tls.c         |  7 +++++++
 libavformat/tls.h         |  5 +++++
 libavformat/tls_gnutls.c  |  7 ++++---
 libavformat/tls_openssl.c |  7 ++++---
 6 files changed, 57 insertions(+), 9 deletions(-)

diff --git a/fftools/ffplay.c b/fftools/ffplay.c
index 28a83e079f..46ba85212a 100644
--- a/fftools/ffplay.c
+++ b/fftools/ffplay.c
@@ -361,6 +361,8 @@ static int64_t audio_callback_time;
 
 #define FF_QUIT_EVENT    (SDL_USEREVENT + 2)
 
+static volatile sig_atomic_t request_quit;
+
 static SDL_Window *window;
 static SDL_Renderer *renderer;
 static SDL_RendererInfo renderer_info = {0};
@@ -1372,7 +1374,7 @@ static void do_exit(VideoState *is)
 
 static void sigterm_handler(int sig)
 {
-    exit(123);
+    request_quit = 1;
 }
 
 static void set_default_window_size(int width, int height, AVRational sar)
@@ -3403,6 +3405,8 @@ static void refresh_loop_wait_event(VideoState *is, 
SDL_Event *event) {
     double remaining_time = 0.0;
     SDL_PumpEvents();
     while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, 
SDL_LASTEVENT)) {
+        if (request_quit)
+            do_exit(is);
         if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > 
CURSOR_HIDE_DELAY) {
             SDL_ShowCursor(0);
             cursor_hidden = 1;
diff --git a/libavformat/rtspdec.c b/libavformat/rtspdec.c
index e0bdf9d4ac..32c38738c4 100644
--- a/libavformat/rtspdec.c
+++ b/libavformat/rtspdec.c
@@ -59,12 +59,42 @@ static const struct RTSPStatusMessage {
     { 0,                          "NULL"                             }
 };
 
+static int rtsp_teardown_interrupt_cb(void *opaque)
+{
+    return av_gettime_relative() >= *(int64_t *)opaque;
+}
+
 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)) {
+        /* Replace the interrupt callback with a short timeout so TEARDOWN
+         * can go out despite a pending Ctrl-C.  For RTSPS both the TLS
+         * URLContext and its TCP child need patching (each holds an
+         * independent copy of the interrupt callback). */
+        int64_t deadline = av_gettime_relative() + 500000LL; /* 500 ms */
+        AVIOInterruptCB timeout_cb = { rtsp_teardown_interrupt_cb, &deadline };
+        URLContext *tcp_child = NULL;
+        AVIOInterruptCB saved_tls = { NULL, NULL };
+        AVIOInterruptCB saved_tcp = { NULL, NULL };
+
+        if (rt->rtsp_hd_out) {
+            saved_tls = rt->rtsp_hd_out->interrupt_callback;
+            rt->rtsp_hd_out->interrupt_callback = timeout_cb;
+            /* For TLS, also patch the underlying TCP child. */
+            tcp_child = ff_tls_get_underlying(rt->rtsp_hd_out);
+            if (tcp_child) {
+                saved_tcp = tcp_child->interrupt_callback;
+                tcp_child->interrupt_callback = timeout_cb;
+            }
+        }
+        ff_rtsp_send_cmd(s, "TEARDOWN", rt->control_uri, NULL, 
&(RTSPMessageHeader){0}, NULL);
+        if (rt->rtsp_hd_out)
+            rt->rtsp_hd_out->interrupt_callback = saved_tls;
+        if (tcp_child)
+            tcp_child->interrupt_callback = saved_tcp;
+    }
 
     ff_rtsp_close_streams(s);
     ff_rtsp_close_connections(s);
diff --git a/libavformat/tls.c b/libavformat/tls.c
index 3eab305f56..f027076b78 100644
--- a/libavformat/tls.c
+++ b/libavformat/tls.c
@@ -122,6 +122,13 @@ int ff_tls_open_underlying(TLSShared *c, URLContext 
*parent, const char *uri, AV
     return ret;
 }
 
+URLContext *ff_tls_get_underlying(URLContext *h)
+{
+    if (!h || !h->prot || strcmp(h->prot->name, "tls") != 0)
+        return NULL;
+    return ((TLSShared *)h->priv_data)->tcp;
+}
+
 /**
  * Read all data from the given URL url and store it in the given buffer bp.
  */
diff --git a/libavformat/tls.h b/libavformat/tls.h
index 971ae5c7a5..5f14bd5fe4 100644
--- a/libavformat/tls.h
+++ b/libavformat/tls.h
@@ -119,6 +119,11 @@ int ff_tls_parse_host(TLSShared *s, char *hostname, int 
hostname_size, int *port
 
 int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, 
AVDictionary **options);
 
+/**
+ * Return the underlying TCP/UDP URLContext of a TLS URLContext, or NULL.
+ */
+URLContext *ff_tls_get_underlying(URLContext *h);
+
 int ff_url_read_all(const char *url, AVBPrint *bp);
 
 int ff_tls_set_external_socket(URLContext *h, URLContext *sock);
diff --git a/libavformat/tls_gnutls.c b/libavformat/tls_gnutls.c
index aedbc66e56..c40c2cd7f3 100644
--- a/libavformat/tls_gnutls.c
+++ b/libavformat/tls_gnutls.c
@@ -475,9 +475,10 @@ static ssize_t gnutls_url_push(gnutls_transport_ptr_t 
transport,
     int ret = ffurl_write(uc, buf, len);
     if (ret >= 0)
         return ret;
-    if (ret == AVERROR_EXIT)
-        return 0;
-    if (ret == AVERROR(EAGAIN)) {
+    if (ret == AVERROR_EXIT) {
+        /* Use EINTR, not 0: returning 0 would cause GnuTLS to busy-spin. */
+        errno = EINTR;
+    } else if (ret == AVERROR(EAGAIN)) {
         errno = EAGAIN;
     } else {
         errno = EIO;
diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c
index 7c1289c595..81540cfeeb 100644
--- a/libavformat/tls_openssl.c
+++ b/libavformat/tls_openssl.c
@@ -570,9 +570,10 @@ static int url_bio_bwrite(BIO *b, const char *buf, int len)
     if (ret >= 0)
         return ret;
     BIO_clear_retry_flags(b);
-    if (ret == AVERROR_EXIT)
-        return 0;
-    if (ret == AVERROR(EAGAIN))
+    if (ret == AVERROR_EXIT) {
+        /* Don't return 0: that signals success and silently drops the data. */
+        c->io_err = ret;
+    } else if (ret == AVERROR(EAGAIN))
         BIO_set_retry_write(b);
     else
         c->io_err = ret;
-- 
2.52.0

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

Reply via email to