Skip to content

Commit

Permalink
detect if client is 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 f2d8c03
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 71 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>
failures of the client to send feedback requests (these “heartbeat
signals” are sent by the client once per second to ask for a response
showing that the server is still online). 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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1179,14 +1179,14 @@ uxplay was put into the background). To quit, use `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
**-reset n** sets a limit of *n* consecutive failures of the
client to send feedback requests (these "heartbeat signals" are sent by the client
once per second to ask for a response showing that the server is still online).
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
20 changes: 10 additions & 10 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1180,16 +1180,16 @@ uxplay was put into the background). To quit, use `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.

**-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.
**-reset n** sets a limit of *n* consecutive failures of the client to
send feedback requests (these "heartbeat signals" are sent by the client
once per second to ask for a response showing that the server is still
online). 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 f2d8c03

Please sign in to comment.