From 3cb20e2da4137ba92c9bbb7fb4e86eb56fb75c62 Mon Sep 17 00:00:00 2001 From: Yeicor Date: Fri, 13 Mar 2026 13:23:23 +0100 Subject: [PATCH] Refine stream-sink: SRT workaround and connectionless mode Remove automatic SRT latency=50 injection and simplify CLI help. Avoid calling avio_close() for srt:// to work around SRT/FFmpeg epoll deadlocks; other protocols are closed normally. Treat udp:// as connectionless and use a single output stream instead of accepting per-client threads --- app/src/cli.c | 25 +++-------- app/src/stream_sink.c | 101 +++++++++++++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 9521a789..8f3c03d9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -961,25 +961,12 @@ static const struct sc_option options[] = { .longopt_id = OPT_STREAM_SINK, .longopt = "stream-sink", .argdesc = "url", - .text = "Stream the device video (and audio, if enabled) as MPEG-TS " - "to the given URL. Tuned for low-latency live streaming.\n" - "\n" - "Supported protocols and auto-applied server settings:\n" - " srt://HOST:PORT SRT (recommended); adds ?mode=listener " - "and ?latency=50 automatically\n" - " tcp://HOST:PORT raw TCP; adds ?listen=1 automatically\n" - " udp://HOST:PORT UDP (lowest latency, unreliable)\n" - " rtp://HOST:PORT RTP over UDP\n" - "Unknown protocols are used as-is (with a warning).\n" - "\n" - "Low-latency client examples (connect after starting scrcpy):\n" - " ffplay -fflags nobuffer -flags low_delay -framedrop " - "-i srt://127.0.0.1:8080\n" - " ffplay -fflags nobuffer -flags low_delay -framedrop " - "-i tcp://127.0.0.1:8080\n" - " ffplay -fflags nobuffer -flags low_delay -framedrop " - "-i udp://127.0.0.1:8080\n" - " VLC: Media > Open Network Stream > srt://127.0.0.1:8080", + .text = "Stream the device video and audio as MPEG-TS to the given URL.\n" + "Supported protocols are srt, udp and tcp.\n" + "The URL is passed to the FFmpeg muxer, so it may contain " + "additional options (e.g. srt://HOST:PORT?latency=200).\n" + "For faster startup of clients, you may want to set " + "--video-codec-options=i-frame-interval:float=1.0." }, { .longopt_id = OPT_V4L2_SINK, diff --git a/app/src/stream_sink.c b/app/src/stream_sink.c index 61479db4..c9033078 100644 --- a/app/src/stream_sink.c +++ b/app/src/stream_sink.c @@ -99,17 +99,6 @@ sc_stream_sink_build_connect_url(const char *url) { } result = tmp; } - // Keep SRT protocol latency low (default 120 ms is too high for live - // screen mirroring; 50 ms is comfortable for LAN). - // Users on high-latency WAN links can override with e.g. ?latency=200. - if (!sc_url_has_param(result, "latency")) { - char *tmp = sc_url_append_param(result, "latency", "50"); - free(result); - if (!tmp) { - return NULL; - } - result = tmp; - } } else if (is_tcp) { // scrcpy acts as the TCP server, waiting for a player to connect if (!sc_url_has_param(result, "listen")) { @@ -121,11 +110,22 @@ sc_stream_sink_build_connect_url(const char *url) { result = tmp; } } - // udp:// and rtp:// are connectionless; no listener mode needed + // udp:// is connectionless; no listener mode needed return result; } +/** + * Check if a URL uses a connectionless protocol (UDP). + * + * For this protocol, only a single output stream is needed, + * not multiple client connections. + */ +static inline bool +sc_stream_sink_is_connectionless(const char *url) { + return !strncmp(url, "udp://", 6); +} + static AVPacket * sc_stream_sink_packet_ref(const AVPacket *packet) { AVPacket *p = av_packet_alloc(); @@ -562,10 +562,24 @@ run_stream_sink_client(void *data) { sc_stream_sink_client_run_stream(client); - // Close this client's network connection. + // WORKAROUND: SRT epoll deadlock on disconnect + // When closing SRT sockets, FFmpeg's interrupt callback and SRT's internal + // epoll management conflict, causing "no sockets to check, this would deadlock". + // Root cause: FFmpeg may call interrupt_callback during avio_close(), but SRT + // has already removed the socket from epoll, causing state inconsistency. + // TODO: Remove this workaround once SRT/FFmpeg fix the socket lifecycle interaction. + // For now, only skip avio_close() for SRT; other protocols are safe. + bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6); + if (client->ctx->pb) { - avio_close(client->ctx->pb); - client->ctx->pb = NULL; + if (is_srt) { + // SRT workaround: don't call avio_close(), let avformat_free_context() handle it + client->ctx->pb = NULL; + } else { + // Safe for TCP, UDP and other protocols + avio_close(client->ctx->pb); + client->ctx->pb = NULL; + } } // Mark as finished so the accept loop can join and free us. @@ -615,9 +629,12 @@ sc_stream_sink_reap_dead_clients(struct sc_stream_sink *sink) { } /** - * Accept loop: initialises the template context once, then repeatedly accepts - * incoming connections, spawning a per-client thread for each. Runs until - * sink->stopped is set (by sc_stream_sink_stop() or device EOS). + * Main streaming loop: initialises the template context once, then either: + * - For connection-oriented protocols (TCP, SRT): repeatedly accepts incoming + * connections, spawning a per-client thread for each. + * - For connectionless protocols (UDP, RTP): creates a single output stream + * and writes all packets to it directly. + * Runs until sink->stopped is set (by sc_stream_sink_stop() or device EOS). */ // Forward declaration: defined below alongside the other packet-sink callbacks. @@ -633,17 +650,35 @@ run_stream_sink(void *data) { goto stop; } + bool is_connectionless = sc_stream_sink_is_connectionless(sink->url); + char *connect_url = sc_stream_sink_build_connect_url(sink->url); if (!connect_url) { goto stop; } - LOGI("Stream sink: listening for clients on %s", sink->url); + if (is_connectionless) { + LOGI("Stream sink: streaming to %s", connect_url); + } else { + LOGI("Stream sink: listening for clients on %s", connect_url); + } + + bool connectionless_done = false; while (!sink->stopped) { + // For connectionless protocols, only attempt one connection + if (is_connectionless && connectionless_done) { + // Keep the single client thread running; just wait until stopped + sc_mutex_lock(&sink->mutex); + while (!sink->stopped) { + sc_cond_wait(&sink->cond, &sink->mutex); + } + sc_mutex_unlock(&sink->mutex); + break; + } + // Reap any client threads that finished since the last iteration. sc_stream_sink_reap_dead_clients(sink); - AVIOInterruptCB int_cb = { .callback = sc_stream_sink_interrupt_cb, .opaque = sink, @@ -673,7 +708,10 @@ run_stream_sink(void *data) { calloc(1, sizeof(struct sc_stream_sink_client)); if (!client) { LOG_OOM(); - avio_close(client_ctx->pb); + bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6); + if (!is_srt) { + avio_close(client_ctx->pb); + } client_ctx->pb = NULL; avformat_free_context(client_ctx); continue; @@ -698,8 +736,7 @@ run_stream_sink(void *data) { // Write the MPEG-TS stream header for this client. if (avformat_write_header(client_ctx, NULL) < 0) { LOGE("Stream sink: failed to write stream header to client"); - avio_close(client_ctx->pb); - client_ctx->pb = NULL; + client_ctx->pb = NULL; // Don't avio_close() - causes SRT epoll issues avformat_free_context(client_ctx); free(client); continue; @@ -723,9 +760,11 @@ run_stream_sink(void *data) { sc_stream_sink_queue_clear(&client->video_queue); sc_stream_sink_queue_clear(&client->audio_queue); sc_mutex_unlock(&sink->mutex); - // avformat_write_header already moved pb ownership; close it. if (client_ctx->pb) { - avio_close(client_ctx->pb); + bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6); + if (!is_srt) { + avio_close(client_ctx->pb); + } client_ctx->pb = NULL; } avformat_free_context(client_ctx); @@ -736,6 +775,13 @@ run_stream_sink(void *data) { } LOGI("Stream sink: client connected on %s", sink->url); + + if (is_connectionless) { + // For connectionless protocols (UDP, RTP), we only need a single + // stream. Mark it as done and the loop will now wait instead of + // trying to accept new connections. + connectionless_done = true; + } } free(connect_url); @@ -762,7 +808,10 @@ stop: struct sc_stream_sink_client *next = head->next; sc_thread_join(&head->thread, NULL); if (head->ctx->pb) { - avio_close(head->ctx->pb); + bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6); + if (!is_srt) { + avio_close(head->ctx->pb); + } head->ctx->pb = NULL; } avformat_free_context(head->ctx);