From 5e55669a0b91e7cffa6a040ec66144246b9eb5f0 Mon Sep 17 00:00:00 2001 From: "F. Duncanh" Date: Fri, 24 Jan 2025 19:18:09 -0500 Subject: [PATCH] detect client gone offline using feedback instead of ntp signal --- README.html | 18 +++++++------- README.md | 10 ++++---- README.txt | 18 +++++++------- lib/raop.c | 5 ---- lib/raop.h | 3 ++- lib/raop_handlers.h | 4 ++- lib/raop_ntp.c | 27 ++++++-------------- lib/raop_ntp.h | 2 +- lib/raop_rtp_mirror.c | 5 ++-- uxplay.1 | 2 +- uxplay.cpp | 57 +++++++++++++++++++++++++++++++------------ 11 files changed, 82 insertions(+), 69 deletions(-) diff --git a/README.html b/README.html index a441989d0..50ae7ffd3 100644 --- a/README.html +++ b/README.html @@ -1159,16 +1159,16 @@

Usage

ctrl-C fg ctrl-C to terminate the image viewer, bring uxplay into the foreground, and terminate it too.

-reset n sets a limit of n consecutive -timeout failures of the client to respond to ntp requests from the -server (these are sent every 3 seconds to check if the client is still -present, and synchronize with it). After n failures, the client -will be presumed to be offline, and the connection will be reset to -allow a new connection. The default value of n is 5; the value -n = 0 means “no limit” on timeouts.

+timeout failures of the client to send feedback requests (these +“heartbeat signal” are sent by the client once per second to check if +the server is still present. After n missing signals, the +client will be presumed to be offline, and the connection will be reset +to allow a new connection. The default value of n is 15 +seconds; the value n = 0 means “no limit”.

-nofreeze closes the video window after a reset due -to ntp timeout (default is to leave window open to allow a smoother -reconection to the same client). This option may be useful in fullscreen -mode.

+to client going offline (default is to leave window open to allow a +smoother reconection to the same client). This option may be useful in +fullscreen mode.

-nc maintains previous UxPlay < 1.45 behavior that does not close the video window when the the client sends the “Stop Mirroring” signal. This option is currently diff --git a/README.md b/README.md index ab3f8723f..7bdd00705 100644 --- a/README.md +++ b/README.md @@ -1180,13 +1180,13 @@ terminate the image viewer, bring `uxplay` into the foreground, and terminate it too. **-reset n** sets a limit of *n* consecutive timeout failures of the -client to respond to ntp requests from the server (these are sent every -3 seconds to check if the client is still present, and synchronize with -it). After *n* failures, the client will be presumed to be offline, and +client to send feedback requests (these "heartbeat signal" are sent by the client +once per second to check if the server is still present. +After *n* missing signals, the client will be presumed to be offline, and the connection will be reset to allow a new connection. The default -value of *n* is 5; the value *n* = 0 means "no limit" on timeouts. +value of *n* is 15 seconds; the value *n* = 0 means "no limit". -**-nofreeze** closes the video window after a reset due to ntp timeout +**-nofreeze** closes the video window after a reset due to client going offline (default is to leave window open to allow a smoother reconection to the same client). This option may be useful in fullscreen mode. diff --git a/README.txt b/README.txt index 01ed68775..43297e474 100644 --- a/README.txt +++ b/README.txt @@ -1181,15 +1181,15 @@ terminate the image viewer, bring `uxplay` into the foreground, and terminate it too. **-reset n** sets a limit of *n* consecutive timeout failures of the -client to respond to ntp requests from the server (these are sent every -3 seconds to check if the client is still present, and synchronize with -it). After *n* failures, the client will be presumed to be offline, and -the connection will be reset to allow a new connection. The default -value of *n* is 5; the value *n* = 0 means "no limit" on timeouts. - -**-nofreeze** closes the video window after a reset due to ntp timeout -(default is to leave window open to allow a smoother reconection to the -same client). This option may be useful in fullscreen mode. +client to send feedback requests (these "heartbeat signal" are sent by +the client once per second to check if the server is still present. +After *n* missing signals, the client will be presumed to be offline, +and the connection will be reset to allow a new connection. The default +value of *n* is 15 seconds; the value *n* = 0 means "no limit". + +**-nofreeze** closes the video window after a reset due to client going +offline (default is to leave window open to allow a smoother reconection +to the same client). This option may be useful in fullscreen mode. **-nc** maintains previous UxPlay \< 1.45 behavior that does **not close** the video window when the the client sends the "Stop Mirroring" diff --git a/lib/raop.c b/lib/raop.c index c275dc1dd..41b2d2d84 100644 --- a/lib/raop.c +++ b/lib/raop.c @@ -64,7 +64,6 @@ struct raop_s { uint8_t clientFPSdata; int audio_delay_micros; - int max_ntp_timeouts; /* for temporary storage of pin during pair-pin start */ unsigned short pin; @@ -554,7 +553,6 @@ raop_init(raop_callbacks_t *callbacks) { /* initialize switch for display of client's streaming data records */ raop->clientFPSdata = 0; - raop->max_ntp_timeouts = 0; raop->audio_delay_micros = 250000; raop->hls_support = false; @@ -662,9 +660,6 @@ int raop_set_plist(raop_t *raop, const char *plist_item, const int value) { } else if (strcmp(plist_item, "clientFPSdata") == 0) { raop->clientFPSdata = (value ? 1 : 0); if ((int) raop->clientFPSdata != value) retval = 1; - } else if (strcmp(plist_item, "max_ntp_timeouts") == 0) { - raop->max_ntp_timeouts = (value > 0 ? value : 0); - if (raop->max_ntp_timeouts != value) retval = 1; } else if (strcmp(plist_item, "audio_delay_micros") == 0) { if (value >= 0 && value <= 10 * SECOND_IN_USECS) { raop->audio_delay_micros = value; diff --git a/lib/raop.h b/lib/raop.h index e0a206205..97e7eb9e7 100644 --- a/lib/raop.h +++ b/lib/raop.h @@ -67,11 +67,12 @@ struct raop_callbacks_s { void (*video_process)(void *cls, raop_ntp_t *ntp, video_decode_struct *data); void (*video_pause)(void *cls); void (*video_resume)(void *cls); + void (*conn_feedback) (void *cls); /* Optional but recommended callback functions */ void (*conn_init)(void *cls); void (*conn_destroy)(void *cls); - void (*conn_reset) (void *cls, int timeouts, bool reset_video); + void (*conn_reset) (void *cls); void (*conn_teardown)(void *cls, bool *teardown_96, bool *teardown_110 ); void (*audio_flush)(void *cls); void (*video_flush)(void *cls); diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h index 3b2c83880..0bc980ea8 100644 --- a/lib/raop_handlers.h +++ b/lib/raop_handlers.h @@ -742,7 +742,7 @@ raop_handler_setup(raop_conn_t *conn, } conn->raop_ntp = raop_ntp_init(conn->raop->logger, &conn->raop->callbacks, remote, conn->remotelen, (unsigned short) timing_rport, &time_protocol); - raop_ntp_start(conn->raop_ntp, &timing_lport, conn->raop->max_ntp_timeouts); + raop_ntp_start(conn->raop_ntp, &timing_lport); conn->raop_rtp = raop_rtp_init(conn->raop->logger, &conn->raop->callbacks, conn->raop_ntp, remote, conn->remotelen, aeskey, aesiv); conn->raop_rtp_mirror = raop_rtp_mirror_init(conn->raop->logger, &conn->raop->callbacks, @@ -983,6 +983,8 @@ raop_handler_feedback(raop_conn_t *conn, char **response_data, int *response_datalen) { logger_log(conn->raop->logger, LOGGER_DEBUG, "raop_handler_feedback"); + /* register receipt of client's "heartbeat" signal */ + conn->raop->callbacks.conn_feedback(conn->raop->callbacks.cls); } static void diff --git a/lib/raop_ntp.c b/lib/raop_ntp.c index 1a932e41f..026f70b7d 100644 --- a/lib/raop_ntp.c +++ b/lib/raop_ntp.c @@ -58,8 +58,6 @@ struct raop_ntp_s { logger_t *logger; raop_callbacks_t callbacks; - int max_ntp_timeouts; - thread_handle_t thread; mutex_handle_t run_mutex; @@ -94,6 +92,8 @@ struct raop_ntp_s { int tsock; timing_protocol_t time_protocol; + bool client_time_received; + }; @@ -153,6 +153,7 @@ raop_ntp_t *raop_ntp_init(logger_t *logger, raop_callbacks_t *callbacks, const c raop_ntp->logger = logger; memcpy(&raop_ntp->callbacks, callbacks, sizeof(raop_callbacks_t)); raop_ntp->timing_rport = timing_rport; + raop_ntp->client_time_received = false; if (raop_ntp_parse_remote(raop_ntp, remote, remote_addr_len) < 0) { free(raop_ntp); @@ -274,8 +275,6 @@ raop_ntp_thread(void *arg) }; raop_ntp_data_t data_sorted[RAOP_NTP_DATA_COUNT]; const unsigned two_pow_n[RAOP_NTP_DATA_COUNT] = {2, 4, 8, 16, 32, 64, 128, 256}; - int timeout_counter = 0; - bool conn_reset = false; bool logger_debug = (logger_get_level(raop_ntp->logger) >= LOGGER_DEBUG); while (1) { @@ -308,20 +307,15 @@ raop_ntp_thread(void *arg) // Read response response_len = recvfrom(raop_ntp->tsock, (char *)response, sizeof(response), 0, NULL, NULL); if (response_len < 0) { - timeout_counter++; char time[30]; - int level = (timeout_counter == 1 ? LOGGER_DEBUG : LOGGER_ERR); ntp_timestamp_to_time(send_time, time, sizeof(time)); - logger_log(raop_ntp->logger, level, "raop_ntp receive timeout %d (limit %d) (request sent %s)", - timeout_counter, raop_ntp->max_ntp_timeouts, time); - if (timeout_counter == raop_ntp->max_ntp_timeouts) { - conn_reset = true; /* client is no longer responding */ - break; - } + logger_log(raop_ntp->logger, LOGGER_DEBUG , "raop_ntp receive timeout (request sent %s)", time); } else { + if (!raop_ntp->client_time_received) { + raop_ntp->client_time_received = true; + } //local time of the server when the NTP response packet returns int64_t t3 = (int64_t) raop_ntp_get_local_time(raop_ntp); - timeout_counter = 0; // Local time of the server when the NTP request packet leaves the server int64_t t0 = (int64_t) byteutils_get_ntp_timestamp(response, 8); @@ -391,15 +385,11 @@ raop_ntp_thread(void *arg) MUTEX_UNLOCK(raop_ntp->run_mutex); logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp exiting thread"); - if (conn_reset && raop_ntp->callbacks.conn_reset) { - const bool video_reset = false; /* leave "frozen video" in place */ - raop_ntp->callbacks.conn_reset(raop_ntp->callbacks.cls, timeout_counter, video_reset); - } return 0; } void -raop_ntp_start(raop_ntp_t *raop_ntp, unsigned short *timing_lport, int max_ntp_timeouts) +raop_ntp_start(raop_ntp_t *raop_ntp, unsigned short *timing_lport) { logger_log(raop_ntp->logger, LOGGER_DEBUG, "raop_ntp starting time"); int use_ipv6 = 0; @@ -407,7 +397,6 @@ raop_ntp_start(raop_ntp_t *raop_ntp, unsigned short *timing_lport, int max_ntp_t assert(raop_ntp); assert(timing_lport); - raop_ntp->max_ntp_timeouts = max_ntp_timeouts; raop_ntp->timing_lport = *timing_lport; MUTEX_LOCK(raop_ntp->run_mutex); diff --git a/lib/raop_ntp.h b/lib/raop_ntp.h index bf2f5bb24..f829f4e42 100644 --- a/lib/raop_ntp.h +++ b/lib/raop_ntp.h @@ -27,7 +27,7 @@ typedef struct raop_ntp_s raop_ntp_t; typedef enum timing_protocol_e { NTP, TP_NONE, TP_OTHER, TP_UNSPECIFIED } timing_protocol_t; -void raop_ntp_start(raop_ntp_t *raop_ntp, unsigned short *timing_lport, int max_ntp_timeouts); +void raop_ntp_start(raop_ntp_t *raop_ntp, unsigned short *timing_lport); void raop_ntp_stop(raop_ntp_t *raop_ntp); diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c index bc88472ef..5b66194bc 100644 --- a/lib/raop_rtp_mirror.c +++ b/lib/raop_rtp_mirror.c @@ -804,9 +804,8 @@ raop_rtp_mirror_thread(void *arg) MUTEX_UNLOCK(raop_rtp_mirror->run_mutex); logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "raop_rtp_mirror exiting TCP thread"); - if (conn_reset && raop_rtp_mirror->callbacks.conn_reset) { - const bool video_reset = false; /* leave "frozen video" showing */ - raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls, 0, video_reset); + if (conn_reset&& raop_rtp_mirror->callbacks.conn_reset) { + raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls); } if (unsupported_codec) { diff --git a/uxplay.1 b/uxplay.1 index ee8d3f6c0..f760cca71 100644 --- a/uxplay.1 +++ b/uxplay.1 @@ -106,7 +106,7 @@ UxPlay 1.71: An open\-source AirPlay mirroring (+ audio streaming) server: .TP \fB\-ca\fI fn \fR In Airplay Audio (ALAC) mode, write cover-art to file fn. .TP -\fB\-reset\fR n Reset after 3n seconds client silence (default 5, 0=never). +\fB\-reset\fR n Reset after n seconds client silence (default n=15, 0=never). .TP \fB\-nofreeze\fR Do NOT leave frozen screen in place after reset. .TP diff --git a/uxplay.cpp b/uxplay.cpp index b96053d36..ea7a34cdc 100644 --- a/uxplay.cpp +++ b/uxplay.cpp @@ -70,7 +70,7 @@ #define DEFAULT_DEBUG_LOG false #define LOWEST_ALLOWED_PORT 1024 #define HIGHEST_PORT 65535 -#define NTP_TIMEOUT_LIMIT 5 +#define MISSED_FEEDBACK_LIMIT 15 #define BT709_FIX "capssetter caps=\"video/x-h264, colorimetry=bt709\"" #define SRGB_FIX " ! video/x-raw,colorimetry=sRGB,format=RGB ! " #ifdef FULL_RANGE_RGB_FIX @@ -104,7 +104,6 @@ static std::string video_parser = "h264parse"; static std::string video_decoder = "decodebin"; static std::string video_converter = "videoconvert"; static bool show_client_FPS_data = false; -static unsigned int max_ntp_timeouts = NTP_TIMEOUT_LIMIT; static FILE *video_dumpfile = NULL; static std::string video_dumpfile_name = "videodump"; static int video_dump_limit = 0; @@ -156,6 +155,8 @@ static std::string url = ""; static guint gst_x11_window_id = 0; static guint gst_hls_position_id = 0; static bool preserve_connections = false; +static guint missed_feedback_limit = MISSED_FEEDBACK_LIMIT; +static guint missed_feedback = 0; /* logging */ @@ -365,6 +366,28 @@ static void dump_video_to_file(unsigned char *data, int datalen) { } } +static gboolean feedback_callback(gpointer loop) { + if (open_connections) { + if (missed_feedback_limit && missed_feedback > missed_feedback_limit) { + LOGI("***ERROR lost connection with client (network problem?)"); + LOGI("%u missed client feedback signals exceeds limit of %u", missed_feedback, missed_feedback_limit); + LOGI(" Sometimes the network connection may recover after a longer delay:\n" + " the default limit n = %d seconds, can be changed with the \"-reset n\" option", MISSED_FEEDBACK_LIMIT); + if (!nofreeze) { + close_window = false; /* leave "frozen" window open if reset_video is false */ + } + raop_stop(raop); + reset_loop = true; + } else if (missed_feedback > 2) { + LOGE("%u missed client feedback signals (expected once per second); client may be offline", missed_feedback); + } + missed_feedback++; + } else { + missed_feedback = 0; + } + return TRUE; +} + static gboolean reset_callback(gpointer loop) { if (reset_loop) { g_main_loop_quit((GMainLoop *) loop); @@ -435,6 +458,8 @@ static void main_loop() { gst_bus_watch_id[i] = (guint) video_renderer_listen((void *)loop, i); } } + missed_feedback = 0; + guint feedback_watch_id = g_timeout_add_seconds(1, (GSourceFunc) feedback_callback, (gpointer) loop); guint reset_watch_id = g_timeout_add(100, (GSourceFunc) reset_callback, (gpointer) loop); guint video_reset_watch_id = g_timeout_add(100, (GSourceFunc) video_reset_callback, (gpointer) loop); guint sigterm_watch_id = g_unix_signal_add(SIGTERM, (GSourceFunc) sigterm_callback, (gpointer) loop); @@ -449,6 +474,7 @@ static void main_loop() { if (sigterm_watch_id > 0) g_source_remove(sigterm_watch_id); if (reset_watch_id > 0) g_source_remove(reset_watch_id); if (video_reset_watch_id > 0) g_source_remove(video_reset_watch_id); + if (feedback_watch_id > 0) g_source_remove(feedback_watch_id); g_main_loop_unref(loop); } @@ -657,7 +683,7 @@ static void print_info (char *name) { printf("-as 0 (or -a) Turn audio off, streamed video only\n"); printf("-al x Audio latency in seconds (default 0.25) reported to client.\n"); printf("-ca In Airplay Audio (ALAC) mode, write cover-art to file \n"); - printf("-reset n Reset after 3n seconds client silence (default %d, 0=never)\n", NTP_TIMEOUT_LIMIT); + printf("-reset n Reset after n seconds of client silence (default n=%d, 0=never)\n", MISSED_FEEDBACK_LIMIT); printf("-nofreeze Do NOT leave frozen screen in place after reset\n"); printf("-nc Do NOT Close video window when client stops mirroring\n"); printf("-nohold Drop current connection when new client connects.\n"); @@ -739,7 +765,7 @@ static bool get_value (const char *str, unsigned int *n) { static bool get_ports (int nports, std::string option, const char * value, unsigned short * const port) { /*valid entries are comma-separated values port_1,port_2,...,port_r, 0 < r <= nports */ /*where ports are distinct, and are in the allowed range. */ - /*missing values are consecutive to last given value (at least one value needed). */ + /*missed values are consecutive to last given value (at least one value needed). */ char *end; unsigned long l; std::size_t pos; @@ -1020,9 +1046,10 @@ static void parse_arguments (int argc, char *argv[]) { } else if (arg == "-FPSdata") { show_client_FPS_data = true; } else if (arg == "-reset") { - max_ntp_timeouts = 0; - if (!get_value(argv[++i], &max_ntp_timeouts)) { - fprintf(stderr, "invalid \"-reset %s\"; -reset n must have n >= 0, default n = %d\n", argv[i], NTP_TIMEOUT_LIMIT); + /* now using feedback (every 1 sec ) instead of ntp timeouts (every 3 secs) to detect offline client and reset connections */ + fprintf(stderr,"*** NOTE CHANGE: -reset n now means reset n seconds (not 3n seconds) after client goes offline\n"); + if (!get_value(argv[++i], &missed_feedback_limit)) { + fprintf(stderr, "invalid \"-reset %s\"; -reset n must have n >= 0, default n = %d seconds\n", argv[i], MISSED_FEEDBACK_LIMIT); exit(1); } } else if (arg == "-vdmp") { @@ -1587,15 +1614,15 @@ extern "C" void conn_destroy (void *cls) { } } -extern "C" void conn_reset (void *cls, int timeouts, bool reset_video) { +extern "C" void conn_feedback (void *cls) { + /* received client heartbeat signal: connection still exists */ + missed_feedback = 0; +} + +extern "C" void conn_reset (void *cls) { LOGI("***ERROR lost connection with client (network problem?)"); - if (timeouts) { - LOGI(" Client no-response limit of %d timeouts (%d seconds) reached:", timeouts, 3*timeouts); - LOGI(" Sometimes the network connection may recover after a longer delay:\n" - " the default timeout limit n = %d can be changed with the \"-reset n\" option", NTP_TIMEOUT_LIMIT); - } if (!nofreeze) { - close_window = reset_video; /* leave "frozen" window open if reset_video is false */ + close_window = false; /* leave "frozen" window open */ } raop_stop(raop); reset_loop = true; @@ -1931,6 +1958,7 @@ static int start_raop_server (unsigned short display[5], unsigned short tcp[3], raop_cbs.conn_init = conn_init; raop_cbs.conn_destroy = conn_destroy; raop_cbs.conn_reset = conn_reset; + raop_cbs.conn_feedback = conn_feedback; raop_cbs.conn_teardown = conn_teardown; raop_cbs.audio_process = audio_process; raop_cbs.video_process = video_process; @@ -1981,7 +2009,6 @@ static int start_raop_server (unsigned short display[5], unsigned short tcp[3], if (display[4]) raop_set_plist(raop, "overscanned", (int) display[4]); if (show_client_FPS_data) raop_set_plist(raop, "clientFPSdata", 1); - raop_set_plist(raop, "max_ntp_timeouts", max_ntp_timeouts); if (audiodelay >= 0) raop_set_plist(raop, "audio_delay_micros", audiodelay); if (require_password) raop_set_plist(raop, "pin", (int) pin); if (hls_support) raop_set_plist(raop, "hls", 1);