Skip to content

Commit 3cb20e2

Browse files
author
Yeicor
committed
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
1 parent b231df8 commit 3cb20e2

2 files changed

Lines changed: 81 additions & 45 deletions

File tree

app/src/cli.c

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -961,25 +961,12 @@ static const struct sc_option options[] = {
961961
.longopt_id = OPT_STREAM_SINK,
962962
.longopt = "stream-sink",
963963
.argdesc = "url",
964-
.text = "Stream the device video (and audio, if enabled) as MPEG-TS "
965-
"to the given URL. Tuned for low-latency live streaming.\n"
966-
"\n"
967-
"Supported protocols and auto-applied server settings:\n"
968-
" srt://HOST:PORT SRT (recommended); adds ?mode=listener "
969-
"and ?latency=50 automatically\n"
970-
" tcp://HOST:PORT raw TCP; adds ?listen=1 automatically\n"
971-
" udp://HOST:PORT UDP (lowest latency, unreliable)\n"
972-
" rtp://HOST:PORT RTP over UDP\n"
973-
"Unknown protocols are used as-is (with a warning).\n"
974-
"\n"
975-
"Low-latency client examples (connect after starting scrcpy):\n"
976-
" ffplay -fflags nobuffer -flags low_delay -framedrop "
977-
"-i srt://127.0.0.1:8080\n"
978-
" ffplay -fflags nobuffer -flags low_delay -framedrop "
979-
"-i tcp://127.0.0.1:8080\n"
980-
" ffplay -fflags nobuffer -flags low_delay -framedrop "
981-
"-i udp://127.0.0.1:8080\n"
982-
" VLC: Media > Open Network Stream > srt://127.0.0.1:8080",
964+
.text = "Stream the device video and audio as MPEG-TS to the given URL.\n"
965+
"Supported protocols are srt, udp and tcp.\n"
966+
"The URL is passed to the FFmpeg muxer, so it may contain "
967+
"additional options (e.g. srt://HOST:PORT?latency=200).\n"
968+
"For faster startup of clients, you may want to set "
969+
"--video-codec-options=i-frame-interval:float=1.0."
983970
},
984971
{
985972
.longopt_id = OPT_V4L2_SINK,

app/src/stream_sink.c

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,6 @@ sc_stream_sink_build_connect_url(const char *url) {
9999
}
100100
result = tmp;
101101
}
102-
// Keep SRT protocol latency low (default 120 ms is too high for live
103-
// screen mirroring; 50 ms is comfortable for LAN).
104-
// Users on high-latency WAN links can override with e.g. ?latency=200.
105-
if (!sc_url_has_param(result, "latency")) {
106-
char *tmp = sc_url_append_param(result, "latency", "50");
107-
free(result);
108-
if (!tmp) {
109-
return NULL;
110-
}
111-
result = tmp;
112-
}
113102
} else if (is_tcp) {
114103
// scrcpy acts as the TCP server, waiting for a player to connect
115104
if (!sc_url_has_param(result, "listen")) {
@@ -121,11 +110,22 @@ sc_stream_sink_build_connect_url(const char *url) {
121110
result = tmp;
122111
}
123112
}
124-
// udp:// and rtp:// are connectionless; no listener mode needed
113+
// udp:// is connectionless; no listener mode needed
125114

126115
return result;
127116
}
128117

118+
/**
119+
* Check if a URL uses a connectionless protocol (UDP).
120+
*
121+
* For this protocol, only a single output stream is needed,
122+
* not multiple client connections.
123+
*/
124+
static inline bool
125+
sc_stream_sink_is_connectionless(const char *url) {
126+
return !strncmp(url, "udp://", 6);
127+
}
128+
129129
static AVPacket *
130130
sc_stream_sink_packet_ref(const AVPacket *packet) {
131131
AVPacket *p = av_packet_alloc();
@@ -562,10 +562,24 @@ run_stream_sink_client(void *data) {
562562

563563
sc_stream_sink_client_run_stream(client);
564564

565-
// Close this client's network connection.
565+
// WORKAROUND: SRT epoll deadlock on disconnect
566+
// When closing SRT sockets, FFmpeg's interrupt callback and SRT's internal
567+
// epoll management conflict, causing "no sockets to check, this would deadlock".
568+
// Root cause: FFmpeg may call interrupt_callback during avio_close(), but SRT
569+
// has already removed the socket from epoll, causing state inconsistency.
570+
// TODO: Remove this workaround once SRT/FFmpeg fix the socket lifecycle interaction.
571+
// For now, only skip avio_close() for SRT; other protocols are safe.
572+
bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6);
573+
566574
if (client->ctx->pb) {
567-
avio_close(client->ctx->pb);
568-
client->ctx->pb = NULL;
575+
if (is_srt) {
576+
// SRT workaround: don't call avio_close(), let avformat_free_context() handle it
577+
client->ctx->pb = NULL;
578+
} else {
579+
// Safe for TCP, UDP and other protocols
580+
avio_close(client->ctx->pb);
581+
client->ctx->pb = NULL;
582+
}
569583
}
570584

571585
// 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) {
615629
}
616630

617631
/**
618-
* Accept loop: initialises the template context once, then repeatedly accepts
619-
* incoming connections, spawning a per-client thread for each. Runs until
620-
* sink->stopped is set (by sc_stream_sink_stop() or device EOS).
632+
* Main streaming loop: initialises the template context once, then either:
633+
* - For connection-oriented protocols (TCP, SRT): repeatedly accepts incoming
634+
* connections, spawning a per-client thread for each.
635+
* - For connectionless protocols (UDP, RTP): creates a single output stream
636+
* and writes all packets to it directly.
637+
* Runs until sink->stopped is set (by sc_stream_sink_stop() or device EOS).
621638
*/
622639

623640
// Forward declaration: defined below alongside the other packet-sink callbacks.
@@ -633,17 +650,35 @@ run_stream_sink(void *data) {
633650
goto stop;
634651
}
635652

653+
bool is_connectionless = sc_stream_sink_is_connectionless(sink->url);
654+
636655
char *connect_url = sc_stream_sink_build_connect_url(sink->url);
637656
if (!connect_url) {
638657
goto stop;
639658
}
640659

641-
LOGI("Stream sink: listening for clients on %s", sink->url);
660+
if (is_connectionless) {
661+
LOGI("Stream sink: streaming to %s", connect_url);
662+
} else {
663+
LOGI("Stream sink: listening for clients on %s", connect_url);
664+
}
665+
666+
bool connectionless_done = false;
642667

643668
while (!sink->stopped) {
669+
// For connectionless protocols, only attempt one connection
670+
if (is_connectionless && connectionless_done) {
671+
// Keep the single client thread running; just wait until stopped
672+
sc_mutex_lock(&sink->mutex);
673+
while (!sink->stopped) {
674+
sc_cond_wait(&sink->cond, &sink->mutex);
675+
}
676+
sc_mutex_unlock(&sink->mutex);
677+
break;
678+
}
679+
644680
// Reap any client threads that finished since the last iteration.
645681
sc_stream_sink_reap_dead_clients(sink);
646-
647682
AVIOInterruptCB int_cb = {
648683
.callback = sc_stream_sink_interrupt_cb,
649684
.opaque = sink,
@@ -673,7 +708,10 @@ run_stream_sink(void *data) {
673708
calloc(1, sizeof(struct sc_stream_sink_client));
674709
if (!client) {
675710
LOG_OOM();
676-
avio_close(client_ctx->pb);
711+
bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6);
712+
if (!is_srt) {
713+
avio_close(client_ctx->pb);
714+
}
677715
client_ctx->pb = NULL;
678716
avformat_free_context(client_ctx);
679717
continue;
@@ -698,8 +736,7 @@ run_stream_sink(void *data) {
698736
// Write the MPEG-TS stream header for this client.
699737
if (avformat_write_header(client_ctx, NULL) < 0) {
700738
LOGE("Stream sink: failed to write stream header to client");
701-
avio_close(client_ctx->pb);
702-
client_ctx->pb = NULL;
739+
client_ctx->pb = NULL; // Don't avio_close() - causes SRT epoll issues
703740
avformat_free_context(client_ctx);
704741
free(client);
705742
continue;
@@ -723,9 +760,11 @@ run_stream_sink(void *data) {
723760
sc_stream_sink_queue_clear(&client->video_queue);
724761
sc_stream_sink_queue_clear(&client->audio_queue);
725762
sc_mutex_unlock(&sink->mutex);
726-
// avformat_write_header already moved pb ownership; close it.
727763
if (client_ctx->pb) {
728-
avio_close(client_ctx->pb);
764+
bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6);
765+
if (!is_srt) {
766+
avio_close(client_ctx->pb);
767+
}
729768
client_ctx->pb = NULL;
730769
}
731770
avformat_free_context(client_ctx);
@@ -736,6 +775,13 @@ run_stream_sink(void *data) {
736775
}
737776

738777
LOGI("Stream sink: client connected on %s", sink->url);
778+
779+
if (is_connectionless) {
780+
// For connectionless protocols (UDP, RTP), we only need a single
781+
// stream. Mark it as done and the loop will now wait instead of
782+
// trying to accept new connections.
783+
connectionless_done = true;
784+
}
739785
}
740786

741787
free(connect_url);
@@ -762,7 +808,10 @@ run_stream_sink(void *data) {
762808
struct sc_stream_sink_client *next = head->next;
763809
sc_thread_join(&head->thread, NULL);
764810
if (head->ctx->pb) {
765-
avio_close(head->ctx->pb);
811+
bool is_srt = sink->url && !strncmp(sink->url, "srt://", 6);
812+
if (!is_srt) {
813+
avio_close(head->ctx->pb);
814+
}
766815
head->ctx->pb = NULL;
767816
}
768817
avformat_free_context(head->ctx);

0 commit comments

Comments
 (0)