Skip to content

Commit

Permalink
detect client gone offline using feedback instead of ntp signal
Browse files Browse the repository at this point in the history
  • Loading branch information
fduncanh committed Jan 25, 2025
1 parent 88b6cb5 commit 5e55669
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 69 deletions.
18 changes: 9 additions & 9 deletions README.html
Original file line number Diff line number Diff line change
Expand Up @@ -1159,16 +1159,16 @@ <h1 id="usage">Usage</h1>
<code>ctrl-C fg ctrl-C</code> to terminate the image viewer, bring
<code>uxplay</code> into the foreground, and terminate it too.</p>
<p><strong>-reset n</strong> sets a limit of <em>n</em> 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 <em>n</em> failures, the client
will be presumed to be offline, and the connection will be reset to
allow a new connection. The default value of <em>n</em> is 5; the value
<em>n</em> = 0 means “no limit” on timeouts.</p>
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 <em>n</em> 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 <em>n</em> is 15
seconds; the value <em>n</em> = 0 means “no limit”.</p>
<p><strong>-nofreeze</strong> 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.</p>
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.</p>
<p><strong>-nc</strong> maintains previous UxPlay &lt; 1.45 behavior
that does <strong>not close</strong> the video window when the the
client sends the “Stop Mirroring” signal. <em>This option is currently
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
18 changes: 9 additions & 9 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 0 additions & 5 deletions lib/raop.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion lib/raop.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion lib/raop_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
27 changes: 8 additions & 19 deletions lib/raop_ntp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -94,6 +92,8 @@ struct raop_ntp_s {
int tsock;

timing_protocol_t time_protocol;
bool client_time_received;

};


Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -391,23 +385,18 @@ 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;

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);
Expand Down
2 changes: 1 addition & 1 deletion lib/raop_ntp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
5 changes: 2 additions & 3 deletions lib/raop_rtp_mirror.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion uxplay.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 42 additions & 15 deletions uxplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down Expand Up @@ -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 <fn> In Airplay Audio (ALAC) mode, write cover-art to file <fn>\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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 5e55669

Please sign in to comment.