From f1c980001aaece74449466a181c52ac443bbd5e1 Mon Sep 17 00:00:00 2001 From: Roman Herbstmann Date: Thu, 10 Sep 2026 23:16:36 +0200 Subject: [PATCH] rtmp/rtsp: unblock a sender stuck in a socket write on disconnect With TCP backpressure (server ACKs not arriving, send window full) the sender blocks in a java.io socket write, which ignores coroutine cancellation. disconnect() stopped the sender first (BaseSender.stop -> job.cancelAndJoin) and closed the socket only afterwards, so the join waited until the network recovered or TCP gave up. A reConnect hung for the whole time. Try a cooperative stop first (keeps the graceful close in the normal case) and, if the sender does not stop within 1 s, close the socket to unblock the write and stop again. RTSP over TCP (interleaved) writes to the same socket and has the same issue. SRT/UDP are not affected (UDP sends do not block on ACKs). --- .../main/java/com/pedro/rtmp/rtmp/RtmpClient.kt | 14 +++++++++++++- .../main/java/com/pedro/rtsp/rtsp/RtspClient.kt | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt index 30d11b879..2de66e272 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt @@ -59,6 +59,9 @@ import java.nio.ByteBuffer import java.util.concurrent.atomic.AtomicLong import javax.net.ssl.TrustManager import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +private val SENDER_STOP_TIMEOUT = 1.seconds /** * Created by pedro on 8/04/21. @@ -566,7 +569,16 @@ class RtmpClient(private val connectChecker: ConnectChecker) { } private suspend fun disconnect(clear: Boolean) { - if (isStreaming) rtmpSender.stop(clear) + if (isStreaming) { + //the sender can be blocked in a socket write (TCP backpressure) that ignores cancellation, + //only closing the socket unblocks it. Try a cooperative stop first to keep the graceful close. + val stopped = withTimeoutOrNull(SENDER_STOP_TIMEOUT) { rtmpSender.stop(clear) } != null + if (!stopped) { + Log.w(TAG, "sender blocked in socket write, closing socket to unblock it") + runCatching { socket?.close() } + rtmpSender.stop(clear) + } + } runCatching { withTimeoutOrNull(100.milliseconds) { socket?.let { commandsManager.sendClose(it) } diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt index 67720bddb..0ef4a8f5e 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt @@ -46,6 +46,9 @@ import java.net.URISyntaxException import java.nio.ByteBuffer import javax.net.ssl.TrustManager import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +private val SENDER_STOP_TIMEOUT = 1.seconds /** * Created by pedro on 10/02/17. @@ -426,7 +429,16 @@ class RtspClient(private val connectChecker: ConnectChecker) { } private suspend fun disconnect(clear: Boolean) { - if (isStreaming) rtspSender.stop() + if (isStreaming) { + //the sender can be blocked in a socket write (TCP backpressure) that ignores cancellation, + //only closing the socket unblocks it. Try a cooperative stop first to keep the graceful close. + val stopped = withTimeoutOrNull(SENDER_STOP_TIMEOUT) { rtspSender.stop() } != null + if (!stopped) { + Log.w(TAG, "sender blocked in socket write, closing socket to unblock it") + runCatching { socket?.close() } + rtspSender.stop() + } + } val error = runCatching { withTimeoutOrNull(100.milliseconds) { socket?.write(commandsManager.createTeardown())