From 2b0da9125a97cc5f6e1f4f127a7cd74768bcc3c3 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Wed, 6 Aug 2025 07:14:33 -0400
Subject: [PATCH 01/58] cleanup the -pw (no argument) random pw option
---
lib/httpd.c | 6 +++---
lib/raop_handlers.h | 34 ++++++++++++++++++++--------------
2 files changed, 23 insertions(+), 17 deletions(-)
diff --git a/lib/httpd.c b/lib/httpd.c
index 9a4b990..bfc554b 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -216,6 +216,8 @@ httpd_remove_connection(httpd_t *httpd, http_connection_t *connection)
int ret = closesocket(connection->socket_fd);
if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in closesocket (close): %d %s", errno, strerror(errno));
+ } else {
+ logger_log(httpd->logger, LOGGER_INFO, "Connection closed on socket %d", connection->socket_fd);
}
connection->socket_fd = 0;
}
@@ -464,7 +466,7 @@ httpd_thread(void *arg)
while (readstart < 8) {
ret = recv(connection->socket_fd, buffer + readstart, sizeof(buffer) - 1 - readstart, 0);
if (ret == 0) {
- logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
+ logger_log(httpd->logger, LOGGER_DEBUG, "client closed connection on socket %d",
connection->socket_fd);
break;
} else if (ret == -1) {
@@ -487,8 +489,6 @@ httpd_thread(void *arg)
} else {
ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
if (ret == 0) {
- logger_log(httpd->logger, LOGGER_INFO, "Connection closed for socket %d",
- connection->socket_fd);
httpd_remove_connection(httpd, connection);
continue;
}
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 4dcfbab..00ce5ac 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -26,7 +26,7 @@
#define AUDIO_SAMPLE_RATE 44100 /* all supported AirPlay audio format use this sample rate */
#define SECOND_IN_USECS 1000000
#define SECOND_IN_NSECS 1000000000
-#define MAX_PW_ATTEMPTS 5
+#define MAX_PW_ATTEMPTS 3
typedef void (*raop_handler_t)(raop_conn_t *, http_request_t *,
http_response_t *, char **, int *);
@@ -593,15 +593,21 @@ raop_handler_setup(raop_conn_t *conn,
/* RFC2617 Digest authentication (md5 hash) of uxplay client-access password, if set */
if (!conn->authenticated && conn->raop->callbacks.passwd) {
size_t pin_len = 4;
- if (conn->raop->random_pw && strncmp(conn->raop->random_pw + pin_len + 1, deviceID, 17)) {
- conn->raop->auth_fail_count = MAX_PW_ATTEMPTS;
+ const char *authorization = NULL;
+ authorization = http_request_get_header(request, "Authorization");
+ if (!authorization) {
+ // if random_pw is set, but client has changed, unset it
+ if (conn->raop->random_pw && strncmp(conn->raop->random_pw + pin_len + 1, deviceID, 17)) {
+ free(conn->raop->random_pw);
+ conn->raop->random_pw = NULL;
+ }
}
int len;
const char *password = conn->raop->callbacks.passwd(conn->raop->callbacks.cls, &len);
// len = -1 means use a random password for this connection; len = 0 means no password
if (len == -1 && conn->raop->random_pw && conn->raop->auth_fail_count >= MAX_PW_ATTEMPTS) {
// change random_pw after MAX_PW_ATTEMPTS failed authentication attempts
- logger_log(conn->raop->logger, LOGGER_INFO, "Too many authentication failures or new client: generate new random password");
+ logger_log(conn->raop->logger, LOGGER_INFO, "Too many authentication failures: generate new random password");
free(conn->raop->random_pw);
conn->raop->random_pw = NULL;
}
@@ -618,10 +624,13 @@ raop_handler_setup(raop_conn_t *conn,
pin[pin_len] = '\0';
snprintf(pin + pin_len + 1, 18, "%s", deviceID);
conn->raop->auth_fail_count = 0;
+ }
+ if (len == -1 && !authorization && conn->raop->random_pw) {
if (conn->raop->callbacks.display_pin) {
- conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, pin);
+ conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, conn->raop->random_pw);
}
- logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", pin);
+ logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", conn->raop->random_pw);
+ conn->raop->auth_fail_count++;
}
if (len && !conn->authenticated) {
if (len == -1) {
@@ -629,20 +638,17 @@ raop_handler_setup(raop_conn_t *conn,
}
char nonce_string[33] = { '\0' };
//bool stale = false; //not implemented
- const char *authorization = NULL;
- authorization = http_request_get_header(request, "Authorization");
- if (authorization) {
+ if (len && authorization) {
char *ptr = strstr(authorization, "nonce=\"") + strlen("nonce=\"");
strncpy(nonce_string, ptr, 32);
const char *method = http_request_get_method(request);
conn->authenticated = pairing_digest_verify(method, authorization, password);
if (!conn->authenticated) {
- conn->raop->auth_fail_count++;
- logger_log(conn->raop->logger, LOGGER_INFO, "*** authentication failure: count = %u", conn->raop->auth_fail_count);
- if (conn->raop->callbacks.display_pin && conn->raop->auth_fail_count > 1) {
- conn->raop->callbacks.display_pin(conn->raop->callbacks.cls, conn->raop->random_pw);
+ // if random_pw is used, the auth_fail_count will be the number of times it is displayed after creation
+ if (len != -1) {
+ conn->raop->auth_fail_count++;
}
- logger_log(conn->raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", conn->raop->random_pw);
+ logger_log(conn->raop->logger, LOGGER_INFO, "*** authentication failure: count = %u", conn->raop->auth_fail_count);
}
if (conn->authenticated) {
//printf("initial authenticatication OK\n");
From 2436081088b2965a13773703e88a25792d034ec3 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Wed, 6 Aug 2025 09:23:35 -0400
Subject: [PATCH 02/58] README update to document -pw changes
---
README.html | 27 +++++++++++++++++----------
README.md | 20 +++++++++++++++-----
README.txt | 25 ++++++++++++++++---------
3 files changed, 48 insertions(+), 24 deletions(-)
diff --git a/README.html b/README.html
index d647f50..6aeef12 100644
--- a/README.html
+++ b/README.html
@@ -1069,16 +1069,23 @@ options -restrict, -block, -allow for more ways to control client
access). (Add a line “reg” in the startup file if you wish to use
this feature.)
-pw [pwd]. (since 1.72). As an alternative
-to -pin, client access can be controlled with a password set when uxplay
-starts (set it in the .uxplay startup file, where it is stored as
-cleartext.) All users must then know this password. This uses HTTP md5
-Digest authentication, which is now regarded as providing weak security,
-but it is only used to validate the uxplay password, and no user
-credentials are exposed. If pwd is not
-specified, a random 4-digit pin code is displayed, and must be entered
-on the client at each new connection. Note: -pin
-and -pw are alternatives: if both are specified at startup, the earlier
-of these two options is discarded.
+to -pin, client access can be controlled with a password. If a password
+pwd (of length at least six characters) is set when uxplay
+starts (usually set in the startup file, where it is stored as
+cleartext), all users must know this password to connect to UxPlay (the
+client prompts for it). This method uses HTTP md5 Digest authentication,
+which is now regarded as providing weak security, but it is only used to
+validate the uxplay password, and no user credentials are exposed. After
+a successful authentication, the client stores the password, and will
+use it initially for future authentications without prompting, so long
+as the UxPlay deviceID has not changed (this initial authentication will
+fail if the UxPlay password has changed). If pwd is
+not specified with the -pw option when UxPlay starts, a
+new random 4-digit pin code is generated and displayed on the UxPlay
+terminal for each new connection, and must be entered
+on the client (there are three chances to enter it, before it is
+changed). Note: -pin and -pw are alternatives: if both are specified
+at startup, the earlier of these two options is discarded.
-vsync [x] (In Mirror mode:) this option
(now the default) uses timestamps to synchronize audio
with video on the server, with an optional audio delay in (decimal)
diff --git a/README.md b/README.md
index 2821164..2817808 100644
--- a/README.md
+++ b/README.md
@@ -1064,13 +1064,23 @@ deregisters the corresponding client (see options -restrict, -block,
the startup file if you wish to use this feature.)*
**-pw** [*pwd*]. (since 1.72). As an alternative to -pin, client access
-can be controlled with a password set when uxplay starts (set it in
-the .uxplay startup file, where it is stored as cleartext.) All users must
-then know this password. This uses HTTP md5 Digest authentication,
+can be controlled with a password. If a password *pwd* (of length at least
+six characters) is set when uxplay
+starts (usually set in the startup file, where it is stored as
+cleartext), all users must know this password to connect to UxPlay
+(the client prompts for it).
+This method uses HTTP md5 Digest authentication,
which is now regarded as providing weak security, but it is only used to
validate the uxplay password, and no user credentials are exposed.
-If *pwd* is **not** specified, a random 4-digit pin code is displayed, and must
-be entered on the client at **each** new connection.
+After a successful authentication, the client stores the password, and will use
+it initially for future authentications without prompting, so long as
+the UxPlay deviceID has not changed (this initial authentication will fail
+if the UxPlay password has changed).
+If *pwd* is **not** specified with the -pw option when UxPlay starts, a
+new random 4-digit pin code is generated and displayed on the UxPlay terminal
+for **each** new connection, and must
+be entered on the client (there are three
+chances to enter it, before it is changed).
_Note: -pin and -pw are alternatives: if both are specified at startup, the
earlier of these two options is discarded._
diff --git a/README.txt b/README.txt
index c2283ef..8e84d2d 100644
--- a/README.txt
+++ b/README.txt
@@ -1083,15 +1083,22 @@ deregisters the corresponding client (see options -restrict, -block,
the startup file if you wish to use this feature.)*
**-pw** \[*pwd*\]. (since 1.72). As an alternative to -pin, client
-access can be controlled with a password set when uxplay starts (set it
-in the .uxplay startup file, where it is stored as cleartext.) All users
-must then know this password. This uses HTTP md5 Digest authentication,
-which is now regarded as providing weak security, but it is only used to
-validate the uxplay password, and no user credentials are exposed. If
-*pwd* is **not** specified, a random 4-digit pin code is displayed, and
-must be entered on the client at **each** new connection. *Note: -pin
-and -pw are alternatives: if both are specified at startup, the earlier
-of these two options is discarded.*
+access can be controlled with a password. If a password *pwd* (of length
+at least six characters) is set when uxplay starts (usually set in the
+startup file, where it is stored as cleartext), all users must know this
+password to connect to UxPlay (the client prompts for it). This method
+uses HTTP md5 Digest authentication, which is now regarded as providing
+weak security, but it is only used to validate the uxplay password, and
+no user credentials are exposed. After a successful authentication, the
+client stores the password, and will use it initially for future
+authentications without prompting, so long as the UxPlay deviceID has
+not changed (this initial authentication will fail if the UxPlay
+password has changed). If *pwd* is **not** specified with the -pw option
+when UxPlay starts, a new random 4-digit pin code is generated and
+displayed on the UxPlay terminal for **each** new connection, and must
+be entered on the client (there are three chances to enter it, before it
+is changed). *Note: -pin and -pw are alternatives: if both are specified
+at startup, the earlier of these two options is discarded.*
**-vsync \[x\]** (In Mirror mode:) this option (**now the default**)
uses timestamps to synchronize audio with video on the server, with an
From 93b2612477b1086262fd1b83be651c35274156a3 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Wed, 6 Aug 2025 16:29:11 -0400
Subject: [PATCH 03/58] uxplay.cpp update number of bus watches for jpeg
renderer
---
uxplay.cpp | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/uxplay.cpp b/uxplay.cpp
index 48f3a3b..f12e1ec 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -464,8 +464,8 @@ static guint g_unix_signal_add(gint signum, GSourceFunc handler, gpointer user_d
#endif
static void main_loop() {
- guint gst_bus_watch_id[2] = { 0 };
- g_assert(n_renderers <= 2);
+ guint gst_bus_watch_id[3] = { 0 };
+ g_assert(n_renderers <= 3);
GMainLoop *loop = g_main_loop_new(NULL,FALSE);
relaunch_video = false;
reset_loop = false;
@@ -474,10 +474,11 @@ static void main_loop() {
if (use_video) {
relaunch_video = true;
if (url.empty()) {
- n_renderers = h265_support ? 2 : 1;
+ /* renderer[0] : jpeg coverart; renderer[1] h264 video; renderer[2] h265 video (optional) */
+ n_renderers = h265_support ? 3 : 2;
gst_x11_window_id = 0;
} else {
- /* hls video will be rendered */
+ /* hls video will be rendered: renderer[0] : hls */
n_renderers = 1;
url.erase();
gst_x11_window_id = g_timeout_add(100, (GSourceFunc) x11_window_callback, (gpointer) loop);
From 532ac612f35af96cff28ecd8384d7c7f0cd87fd0 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Thu, 7 Aug 2025 06:10:19 -0400
Subject: [PATCH 04/58] add message handler for audio pipeline bus
---
renderers/audio_renderer.c | 43 +++++++++++++++++++++++++++++++++++++-
renderers/audio_renderer.h | 2 +-
renderers/video_renderer.c | 8 +++----
uxplay.cpp | 31 +++++++++++++++++++--------
4 files changed, 69 insertions(+), 15 deletions(-)
diff --git a/renderers/audio_renderer.c b/renderers/audio_renderer.c
index c390e68..5075018 100644
--- a/renderers/audio_renderer.c
+++ b/renderers/audio_renderer.c
@@ -45,6 +45,7 @@ typedef struct audio_renderer_s {
GstElement *appsrc;
GstElement *pipeline;
GstElement *volume;
+ GstBus *bus;
unsigned char ct;
} audio_renderer_t ;
static audio_renderer_t *renderer_type[NFORMATS];
@@ -186,7 +187,7 @@ void audio_renderer_init(logger_t *render_logger, const char* audiosink, const b
g_assert (renderer_type[i]->pipeline);
gst_pipeline_use_clock(GST_PIPELINE_CAST(renderer_type[i]->pipeline), clock);
-
+ renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
renderer_type[i]->appsrc = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "audio_source");
renderer_type[i]->volume = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "volume");
switch (i) {
@@ -367,6 +368,8 @@ void audio_renderer_flush() {
void audio_renderer_destroy() {
audio_renderer_stop();
for (int i = 0; i < NFORMATS ; i++ ) {
+ gst_object_unref (renderer_type[i]->bus);
+ renderer_type[i]->bus = NULL;
gst_object_unref (renderer_type[i]->volume);
renderer_type[i]->volume = NULL;
gst_object_unref (renderer_type[i]->appsrc);
@@ -376,3 +379,41 @@ void audio_renderer_destroy() {
free(renderer_type[i]);
}
}
+
+static gboolean gstreamer_audio_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
+ switch (GST_MESSAGE_TYPE(message)) {
+ case GST_MESSAGE_ERROR: {
+ GError *err;
+ gchar *debug;
+ gst_message_parse_error (message, &err, &debug);
+ logger_log(logger, LOGGER_INFO, "GStreamer error (audio): %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
+ g_error_free(err);
+ g_free(debug);
+ if (renderer->appsrc) {
+ gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
+ }
+ gst_bus_set_flushing(bus, TRUE);
+ gst_element_set_state (renderer->pipeline, GST_STATE_READY);
+ g_main_loop_quit( (GMainLoop *) loop);
+ break;
+ }
+ case GST_MESSAGE_EOS:
+ logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream (audio)");
+ break;
+ case GST_MESSAGE_ELEMENT:
+ // many "level" messages may be sent
+ break;
+ default:
+ /* unhandled message */
+ logger_log(logger, LOGGER_DEBUG,"GStreamer unhandled audio bus message: src = %s type = %s",
+ GST_MESSAGE_SRC_NAME(message), GST_MESSAGE_TYPE_NAME(message));
+ break;
+ }
+ return TRUE;
+}
+
+unsigned int audio_renderer_listen(void *loop, int id) {
+ g_assert(id >= 0 && id < NFORMATS);
+ return (unsigned int) gst_bus_add_watch(renderer_type[id]->bus,(GstBusFunc)
+ gstreamer_audio_pipeline_bus_callback, (gpointer) loop);
+}
diff --git a/renderers/audio_renderer.h b/renderers/audio_renderer.h
index 232da28..d981e20 100644
--- a/renderers/audio_renderer.h
+++ b/renderers/audio_renderer.h
@@ -40,7 +40,7 @@ void audio_renderer_render_buffer(unsigned char* data, int *data_len, unsigned s
void audio_renderer_set_volume(double volume);
void audio_renderer_flush();
void audio_renderer_destroy();
-
+unsigned int audio_renderer_listen(void *loop, int id);
#ifdef __cplusplus
}
#endif
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index c701c88..7e04753 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -647,7 +647,7 @@ static void get_stream_status_name(GstStreamStatusType type, char *name, size_t
}
}
-gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
+static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
GstState old_state, new_state;
const gchar no_state[] = "";
const gchar *old_state_name = no_state, *new_state_name = no_state;
@@ -756,7 +756,7 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
gchar *debug;
gboolean flushing;
gst_message_parse_error (message, &err, &debug);
- logger_log(logger, LOGGER_INFO, "GStreamer error: %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
+ logger_log(logger, LOGGER_INFO, "GStreamer error (video): %s %s", GST_MESSAGE_SRC_NAME(message),err->message);
if (!hls_video && strstr(err->message,"Internal data stream error")) {
logger_log(logger, LOGGER_INFO,
"*** This is a generic GStreamer error that usually means that GStreamer\n"
@@ -780,7 +780,7 @@ gboolean gstreamer_pipeline_bus_callback(GstBus *bus, GstMessage *message, void
}
case GST_MESSAGE_EOS:
/* end-of-stream */
- logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream");
+ logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream (video)");
if (hls_video) {
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
@@ -1007,5 +1007,5 @@ void video_renderer_seek(float position) {
unsigned int video_renderer_listen(void *loop, int id) {
g_assert(id >= 0 && id < n_renderers);
return (unsigned int) gst_bus_add_watch(renderer_type[id]->bus,(GstBusFunc)
- gstreamer_pipeline_bus_callback, (gpointer) loop);
+ gstreamer_video_pipeline_bus_callback, (gpointer) loop);
}
diff --git a/uxplay.cpp b/uxplay.cpp
index f12e1ec..c3b41fb 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -164,7 +164,8 @@ static double db_high = 0.0;
static bool taper_volume = false;
static double initial_volume = 0.0;
static bool h265_support = false;
-static int n_renderers = 0;
+static int n_video_renderers = 0;
+static int n_audio_renderers = 0;
static bool hls_support = false;
static std::string url = "";
static guint gst_x11_window_id = 0;
@@ -464,8 +465,10 @@ static guint g_unix_signal_add(gint signum, GSourceFunc handler, gpointer user_d
#endif
static void main_loop() {
- guint gst_bus_watch_id[3] = { 0 };
- g_assert(n_renderers <= 3);
+ guint gst_video_bus_watch_id[3] = { 0 };
+ g_assert(n_video_renderers <= 3);
+ guint gst_audio_bus_watch_id[2] = { 0 };
+ g_assert(n_audio_renderers <= 2);
GMainLoop *loop = g_main_loop_new(NULL,FALSE);
relaunch_video = false;
reset_loop = false;
@@ -475,18 +478,25 @@ static void main_loop() {
relaunch_video = true;
if (url.empty()) {
/* renderer[0] : jpeg coverart; renderer[1] h264 video; renderer[2] h265 video (optional) */
- n_renderers = h265_support ? 3 : 2;
+ n_video_renderers = h265_support ? 3 : 2;
gst_x11_window_id = 0;
} else {
/* hls video will be rendered: renderer[0] : hls */
- n_renderers = 1;
+ n_video_renderers = 1;
url.erase();
gst_x11_window_id = g_timeout_add(100, (GSourceFunc) x11_window_callback, (gpointer) loop);
}
- for (int i = 0; i < n_renderers; i++) {
- gst_bus_watch_id[i] = (guint) video_renderer_listen((void *)loop, i);
+ for (int i = 0; i < n_video_renderers; i++) {
+ gst_video_bus_watch_id[i] = (guint) video_renderer_listen((void *)loop, i);
}
}
+ if (use_audio) {
+ n_audio_renderers = 2;
+ for (int i = 0; i < n_audio_renderers; i++) {
+ gst_audio_bus_watch_id[i] = (guint) audio_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);
@@ -495,8 +505,11 @@ static void main_loop() {
guint sigint_watch_id = g_unix_signal_add(SIGINT, (GSourceFunc) sigint_callback, (gpointer) loop);
g_main_loop_run(loop);
- for (int i = 0; i < n_renderers; i++) {
- if (gst_bus_watch_id[i] > 0) g_source_remove(gst_bus_watch_id[i]);
+ for (int i = 0; i < n_video_renderers; i++) {
+ if (gst_video_bus_watch_id[i] > 0) g_source_remove(gst_video_bus_watch_id[i]);
+ }
+ for (int i = 0; i < n_audio_renderers; i++) {
+ if (gst_audio_bus_watch_id[i] > 0) g_source_remove(gst_audio_bus_watch_id[i]);
}
if (gst_x11_window_id > 0) g_source_remove(gst_x11_window_id);
if (sigint_watch_id > 0) g_source_remove(sigint_watch_id);
From 56bdfbbda8731d6186c9ca5faea9a2b523cca2fa Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Thu, 7 Aug 2025 07:09:56 -0400
Subject: [PATCH 05/58] video_renderer.c: add bus to renderer in
video_renderer_init
---
renderers/video_renderer.c | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 7e04753..1ba220f 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -267,6 +267,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
renderer_type[i]->autovideo = auto_videosink;
renderer_type[i]->id = i;
renderer_type[i]->bus = NULL;
+ renderer_type[i]->appsrc = NULL;
if (hls_video) {
/* use playbin3 to play HLS video: replace "playbin3" by "playbin" to use playbin2 */
switch (playbin_version) {
@@ -282,7 +283,6 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
}
logger_log(logger, LOGGER_INFO, "Will use GStreamer playbin version %u to play HLS streamed video", playbin_version);
g_assert(renderer_type[i]->pipeline);
- renderer_type[i]->appsrc = NULL;
renderer_type[i]->codec = hls;
/* if we are not using an autovideosink, build a videosink based on the string "videosink" */
if (!auto_videosink) {
@@ -377,7 +377,6 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
g_clear_error (&error);
}
g_assert (renderer_type[i]->pipeline);
-
GstClock *clock = gst_system_clock_obtain();
g_object_set(clock, "clock-type", GST_CLOCK_TYPE_REALTIME, NULL);
gst_pipeline_use_clock(GST_PIPELINE_CAST(renderer_type[i]->pipeline), clock);
@@ -417,6 +416,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
}
}
#endif
+ renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_READY);
GstState state;
if (gst_element_get_state (renderer_type[i]->pipeline, &state, NULL, 100 * GST_MSECOND)) {
@@ -461,7 +461,6 @@ void video_renderer_start() {
GstState state;
const gchar *state_name;
if (hls_video) {
- renderer->bus = gst_element_get_bus(renderer->pipeline);
gst_element_set_state (renderer->pipeline, GST_STATE_PAUSED);
gst_element_get_state(renderer->pipeline, &state, NULL, 1000 * GST_MSECOND);
state_name= gst_element_state_get_name(state);
@@ -470,7 +469,6 @@ void video_renderer_start() {
}
/* when not hls, start both h264 and h265 pipelines; will shut down the "wrong" one when we know the codec */
for (int i = 0; i < n_renderers; i++) {
- renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_PAUSED);
gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
state_name= gst_element_state_get_name(state);
From 3e1d83d7d68a63c5df7b2ad1b8747c9b0adfdd67 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Thu, 7 Aug 2025 16:14:18 -0400
Subject: [PATCH 06/58] cleanup in video_renderer.c
---
renderers/video_renderer.c | 27 +++++++++++++++++----------
uxplay.cpp | 2 +-
2 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 1ba220f..4086312 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -472,7 +472,7 @@ void video_renderer_start() {
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_PAUSED);
gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
state_name= gst_element_state_get_name(state);
- logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d state %s", i, state_name);
+ logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d %p state %s", i, renderer_type[i], state_name);
}
renderer = NULL;
first_packet = true;
@@ -537,6 +537,10 @@ uint64_t video_renderer_render_buffer(unsigned char* data, int *data_len, int *n
logger_log(logger, LOGGER_INFO, "Begin streaming to GStreamer video pipeline");
first_packet = false;
}
+ if (!renderer || !(renderer->appsrc)) {
+ logger_log(logger, LOGGER_DEBUG, "*** no video renderer found");
+ return 0;
+ }
buffer = gst_buffer_new_allocate(NULL, *data_len, NULL);
g_assert(buffer != NULL);
//g_print("video latency %8.6f\n", (double) latency / SECOND_IN_NSECS);
@@ -582,21 +586,23 @@ static void video_renderer_destroy_instance(video_renderer_t *renderer) {
GstState state;
GstStateChangeReturn ret;
gst_element_get_state(renderer->pipeline, &state, NULL, 100 * GST_MSECOND);
- logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
+ logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
if (state != GST_STATE_NULL) {
if (!hls_video) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
}
ret = gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
- logger_log(logger, LOGGER_DEBUG,"pipeline_state_change_return: %s",
- gst_element_state_change_return_get_name(ret));
- gst_element_get_state(renderer->pipeline, NULL, NULL, 1000 * GST_MSECOND);
+ logger_log(logger, LOGGER_DEBUG,"pipeline_state_change_return: %s",
+ gst_element_state_change_return_get_name(ret));
+ gst_element_get_state(renderer->pipeline, &state, NULL, 1000 * GST_MSECOND);
+ logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
+ }
+ if (renderer->appsrc) {
+ gst_object_unref (renderer->appsrc);
+ renderer->appsrc = NULL;
}
gst_object_unref(renderer->bus);
- if (renderer->appsrc) {
- gst_object_unref (renderer->appsrc);
- }
- gst_object_unref (renderer->pipeline);
+ gst_object_unref(renderer->pipeline);
#ifdef X_DISPLAY_FIX
if (renderer->gst_window) {
free(renderer->gst_window);
@@ -605,6 +611,7 @@ static void video_renderer_destroy_instance(video_renderer_t *renderer) {
#endif
free (renderer);
renderer = NULL;
+ logger_log(logger, LOGGER_DEBUG,"renderer destroyed\n");
}
}
@@ -908,7 +915,7 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
logger_log(logger, LOGGER_INFO, "*** video format is h265 high definition (HD/4K) video %dx%d", width, height);
}
/* destroy unused renderers */
- for (int i = 1; i < n_renderers; i++) {
+ for (int i = 0; i < n_renderers; i++) {
if (renderer_type[i] == renderer) {
continue;
}
diff --git a/uxplay.cpp b/uxplay.cpp
index c3b41fb..c276103 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -2632,7 +2632,7 @@ int main (int argc, char *argv[]) {
url.erase();
raop_remove_known_connections(raop);
}
- const char *uri = (url.empty() ? NULL : url.c_str());
+ const char *uri = (url.empty() ? NULL : url.c_str());
video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
videosink_options.c_str(), fullscreen, video_sync, h265_support, playbin_version, uri);
From c8ce350226473576959ea1165cdc2ad03394372f Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 8 Aug 2025 11:03:47 -0400
Subject: [PATCH 07/58] raop : whitespace cleanup
---
lib/raop_handlers.h | 365 +++++++++++++++++++++---------------------
lib/raop_ntp.c | 2 +-
lib/raop_rtp.c | 45 +++---
lib/raop_rtp_mirror.c | 52 +++---
4 files changed, 233 insertions(+), 231 deletions(-)
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 00ce5ac..1e7649b 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -60,9 +60,9 @@ raop_handler_info(raop_conn_t *conn,
printf("qualifier: %s\n", qualifier_string);
txtAirPlay = true;
}
- if (qualifier_string) {
+ if (qualifier_string) {
free(qualifier_string);
- }
+ }
}
#endif
plist_t res_node = plist_new_dict();
@@ -265,9 +265,9 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
if (PLIST_IS_STRING(req_method_node) && PLIST_IS_STRING(req_user_node)) {
/* this is the initial pair-setup-pin request */
const char *salt;
- char pin[6];
- const char *pk;
- int len_pk, len_salt;
+ char pin[6];
+ const char *pk;
+ int len_pk, len_salt;
char *method = NULL;
char *user = NULL;
plist_get_string_val(req_method_node, &method);
@@ -280,13 +280,13 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
return;
}
free (method);
- plist_get_string_val(req_user_node, &user);
+ plist_get_string_val(req_user_node, &user);
logger_log(conn->raop->logger, LOGGER_INFO, "pair-setup-pin: device_id = %s", user);
snprintf(pin, 6, "%04u", conn->raop->pin % 10000);
if (conn->raop->pin < 10000) {
conn->raop->pin = 0;
}
- int ret = srp_new_user(conn->session, conn->raop->pairing, (const char *) user,
+ int ret = srp_new_user(conn->session, conn->raop->pairing, (const char *) user,
(const char *) pin, &salt, &len_salt, &pk, &len_pk);
free(user);
plist_free(req_root_node);
@@ -302,19 +302,19 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- return;
+ return;
} else if (PLIST_IS_DATA(req_pk_node) && PLIST_IS_DATA(req_proof_node)) {
/* this is the second part of pair-setup-pin request */
char *client_pk = NULL;
char *client_proof = NULL;
- unsigned char proof[64];
- memset(proof, 0, sizeof(proof));
+ unsigned char proof[64];
+ memset(proof, 0, sizeof(proof));
uint64_t client_pk_len;
uint64_t client_proof_len;
plist_get_data_val(req_pk_node, &client_pk, &client_pk_len);
plist_get_data_val(req_proof_node, &client_proof, &client_proof_len);
if (logger_debug) {
- char *str = utils_data_to_string((const unsigned char *) client_proof, client_proof_len, 20);
+ char *str = utils_data_to_string((const unsigned char *) client_proof, client_proof_len, 20);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client SRP6a proof :\n%s", str);
free (str);
}
@@ -339,7 +339,7 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- return;
+ return;
} else if (PLIST_IS_DATA(req_epk_node) && PLIST_IS_DATA(req_authtag_node)) {
/* this is the third part of pair-setup-pin request */
char *client_epk = NULL;
@@ -347,25 +347,25 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
uint64_t client_epk_len;
uint64_t client_authtag_len;
unsigned char epk[ED25519_KEY_SIZE];
- unsigned char authtag[GCM_AUTHTAG_SIZE];
- int ret;
+ unsigned char authtag[GCM_AUTHTAG_SIZE];
+ int ret;
plist_get_data_val(req_epk_node, &client_epk, &client_epk_len);
plist_get_data_val(req_authtag_node, &client_authtag, &client_authtag_len);
- if (logger_debug) {
+ if (logger_debug) {
char *str = utils_data_to_string((const unsigned char *) client_epk, client_epk_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client_epk %d:\n%s\n", (int) client_epk_len, str);
str = utils_data_to_string((const unsigned char *) client_authtag, client_authtag_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "client_authtag %d:\n%s\n", (int) client_authtag_len, str);
free (str);
- }
+ }
- memcpy(epk, client_epk, ED25519_KEY_SIZE);
- memcpy(authtag, client_authtag, GCM_AUTHTAG_SIZE);
+ memcpy(epk, client_epk, ED25519_KEY_SIZE);
+ memcpy(authtag, client_authtag, GCM_AUTHTAG_SIZE);
free (client_authtag);
free (client_epk);
plist_free(req_root_node);
- ret = srp_confirm_pair_setup(conn->session, conn->raop->pairing, epk, authtag);
+ ret = srp_confirm_pair_setup(conn->session, conn->raop->pairing, epk, authtag);
if (ret < 0) {
logger_log(conn->raop->logger, LOGGER_ERR, "pair-pin-setup (step 3): client authentication failed\n");
goto authentication_failed;
@@ -375,13 +375,13 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
pairing_session_set_setup_status(conn->session);
plist_t res_root_node = plist_new_dict();
plist_t res_epk_node = plist_new_data((const char *) epk, 32);
- plist_t res_authtag_node = plist_new_data((const char *) authtag, 16);
+ plist_t res_authtag_node = plist_new_data((const char *) authtag, 16);
plist_dict_set_item(res_root_node, "epk", res_epk_node);
plist_dict_set_item(res_root_node, "authTag", res_authtag_node);
plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen);
plist_free(res_root_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- return;
+ return;
}
authentication_failed:;
http_response_init(response, "RTSP/1.0", 470, "Client Authentication Failure");
@@ -439,58 +439,58 @@ raop_handler_pairverify(raop_conn_t *conn,
return;
}
switch (data[0]) {
- case 1:
- if (datalen != 4 + X25519_KEY_SIZE + ED25519_KEY_SIZE) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
- return;
- }
- /* We can fall through these errors, the result will just be garbage... */
- if (pairing_session_handshake(conn->session, data + 4, data + 4 + X25519_KEY_SIZE)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Error initializing pair-verify handshake");
- }
- if (pairing_session_get_public_key(conn->session, public_key)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ECDH public key");
- }
- if (pairing_session_get_signature(conn->session, signature)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ED25519 signature");
- }
- if (register_check) {
- bool registered_client = true;
- if (conn->raop->callbacks.check_register) {
- const unsigned char *pk = data + 4 + X25519_KEY_SIZE;
- char *pk64;
- ed25519_pk_to_base64(pk, &pk64);
- registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk64);
- free (pk64);
- }
-
- if (!registered_client) {
- return;
- }
- }
- *response_data = malloc(sizeof(public_key) + sizeof(signature));
- if (*response_data) {
- http_response_add_header(response, "Content-Type", "application/octet-stream");
- memcpy(*response_data, public_key, sizeof(public_key));
- memcpy(*response_data + sizeof(public_key), signature, sizeof(signature));
- *response_datalen = sizeof(public_key) + sizeof(signature);
- }
- break;
- case 0:
- logger_log(conn->raop->logger, LOGGER_DEBUG, "2nd pair-verify step: checking signature");
- if (datalen != 4 + PAIRING_SIG_SIZE) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
- return;
+ case 1:
+ if (datalen != 4 + X25519_KEY_SIZE + ED25519_KEY_SIZE) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
+ return;
+ }
+ /* We can fall through these errors, the result will just be garbage... */
+ if (pairing_session_handshake(conn->session, data + 4, data + 4 + X25519_KEY_SIZE)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Error initializing pair-verify handshake");
+ }
+ if (pairing_session_get_public_key(conn->session, public_key)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ECDH public key");
+ }
+ if (pairing_session_get_signature(conn->session, signature)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Error getting ED25519 signature");
+ }
+ if (register_check) {
+ bool registered_client = true;
+ if (conn->raop->callbacks.check_register) {
+ const unsigned char *pk = data + 4 + X25519_KEY_SIZE;
+ char *pk64;
+ ed25519_pk_to_base64(pk, &pk64);
+ registered_client = conn->raop->callbacks.check_register(conn->raop->callbacks.cls, pk64);
+ free (pk64);
}
- if (pairing_session_finish(conn->session, data + 4)) {
- logger_log(conn->raop->logger, LOGGER_ERR, "Incorrect pair-verify signature");
- http_response_set_disconnect(response, 1);
+ if (!registered_client) {
return;
}
- logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-verify: signature is verified");
+ }
+ *response_data = malloc(sizeof(public_key) + sizeof(signature));
+ if (*response_data) {
http_response_add_header(response, "Content-Type", "application/octet-stream");
- break;
+ memcpy(*response_data, public_key, sizeof(public_key));
+ memcpy(*response_data + sizeof(public_key), signature, sizeof(signature));
+ *response_datalen = sizeof(public_key) + sizeof(signature);
+ }
+ break;
+ case 0:
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "2nd pair-verify step: checking signature");
+ if (datalen != 4 + PAIRING_SIG_SIZE) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Invalid pair-verify data");
+ return;
+ }
+
+ if (pairing_session_finish(conn->session, data + 4)) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Incorrect pair-verify signature");
+ http_response_set_disconnect(response, 1);
+ return;
+ }
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "pair-verify: signature is verified");
+ http_response_add_header(response, "Content-Type", "application/octet-stream");
+ break;
}
}
@@ -618,7 +618,7 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Failed to generate random pin");
pin_4 = 1234;
}
- conn->raop->random_pw = (char *) malloc(pin_len + 1 + 18);
+ conn->raop->random_pw = (char *) malloc(pin_len + 1 + 18);
char *pin = conn->raop->random_pw;
snprintf(pin, pin_len + 1, "%04u", pin_4 % 10000);
pin[pin_len] = '\0';
@@ -661,7 +661,7 @@ raop_handler_setup(raop_conn_t *conn,
if (conn->authenticated && conn->raop->random_pw) {
free (conn->raop->random_pw);
conn->raop->random_pw = NULL;
- }
+ }
if (conn->raop->nonce) {
free(conn->raop->nonce);
conn->raop->nonce = NULL;
@@ -691,17 +691,17 @@ raop_handler_setup(raop_conn_t *conn,
char* eiv = NULL;
uint64_t eiv_len = 0;
- char *model = NULL;
+ char *model = NULL;
char *name = NULL;
bool admit_client = true;
plist_t req_model_node = plist_dict_get_item(req_root_node, "model");
plist_get_string_val(req_model_node, &model);
plist_t req_name_node = plist_dict_get_item(req_root_node, "name");
plist_get_string_val(req_name_node, &name);
- if (conn->raop->callbacks.report_client_request) {
+ if (conn->raop->callbacks.report_client_request) {
conn->raop->callbacks.report_client_request(conn->raop->callbacks.cls, deviceID, model, name, &admit_client);
}
- if (admit_client && deviceID && name && conn->raop->callbacks.register_client) {
+ if (admit_client && deviceID && name && conn->raop->callbacks.register_client) {
char *client_device_id = NULL;
char *client_pk = NULL; /* encoded as null-terminated base64 string, must be freed*/
get_pairing_session_client_data(conn->session, &client_device_id, &client_pk);
@@ -709,7 +709,7 @@ raop_handler_setup(raop_conn_t *conn,
conn->raop->callbacks.register_client(conn->raop->callbacks.cls, client_device_id, client_pk, name);
free (client_pk);
}
- }
+ }
if (deviceID) {
free (deviceID);
deviceID = NULL;
@@ -727,17 +727,17 @@ raop_handler_setup(raop_conn_t *conn,
plist_free(res_root_node);
plist_free(req_root_node);
return;
- }
+ }
plist_get_data_val(req_eiv_node, &eiv, &eiv_len);
memcpy(aesiv, eiv, 16);
free(eiv);
logger_log(conn->raop->logger, LOGGER_DEBUG, "eiv_len = %llu", eiv_len);
- if (logger_debug) {
+ if (logger_debug) {
char* str = utils_data_to_string(aesiv, 16, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "16 byte aesiv (needed for AES-CBC audio decryption iv):\n%s", str);
free(str);
- }
+ }
char* ekey = NULL;
uint64_t ekey_len = 0;
@@ -746,7 +746,7 @@ raop_handler_setup(raop_conn_t *conn,
free(ekey);
logger_log(conn->raop->logger, LOGGER_DEBUG, "ekey_len = %llu", ekey_len);
// eaeskey is 72 bytes, aeskey is 16 bytes
- if (logger_debug) {
+ if (logger_debug) {
char *str = utils_data_to_string((unsigned char *) eaeskey, ekey_len, 16);
logger_log(conn->raop->logger, LOGGER_DEBUG, "ekey:\n%s", str);
free (str);
@@ -814,9 +814,9 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Client specified AirPlay2 \"Remote Control\" protocol\n"
" Only AirPlay v1 protocol (using NTP and timing port) is supported");
}
- }
+ }
char *timing_protocol = NULL;
- timing_protocol_t time_protocol = TP_NONE;
+ timing_protocol_t time_protocol = TP_NONE;
plist_t req_timing_protocol_node = plist_dict_get_item(req_root_node, "timingProtocol");
plist_get_string_val(req_timing_protocol_node, &timing_protocol);
if (timing_protocol) {
@@ -841,7 +841,7 @@ raop_handler_setup(raop_conn_t *conn,
}
uint64_t timing_rport = 0;
plist_t req_timing_port_node = plist_dict_get_item(req_root_node, "timingPort");
- if (req_timing_port_node) {
+ if (req_timing_port_node) {
plist_get_uint_val(req_timing_port_node, &timing_rport);
}
if (timing_rport) {
@@ -897,106 +897,107 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_DEBUG, "type = %llu", type);
switch (type) {
- case 110: {
- // Mirroring
- unsigned short dport = conn->raop->mirror_data_lport;
- plist_t stream_id_node = plist_dict_get_item(req_stream_node, "streamConnectionID");
- uint64_t stream_connection_id = 0;
- plist_get_uint_val(stream_id_node, &stream_connection_id);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "streamConnectionID (needed for AES-CTR video decryption"
- " key and iv): %llu", stream_connection_id);
+ case 110: {
+ // Mirroring
+ unsigned short dport = conn->raop->mirror_data_lport;
+ plist_t stream_id_node = plist_dict_get_item(req_stream_node, "streamConnectionID");
+ uint64_t stream_connection_id = 0;
+ plist_get_uint_val(stream_id_node, &stream_connection_id);
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "streamConnectionID (needed for AES-CTR video decryption"
+ " key and iv): %llu", stream_connection_id);
- if (conn->raop_rtp_mirror) {
- raop_rtp_mirror_init_aes(conn->raop_rtp_mirror, &stream_connection_id);
- raop_rtp_mirror_start(conn->raop_rtp_mirror, &dport, conn->raop->clientFPSdata);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "Mirroring initialized successfully");
- } else {
- logger_log(conn->raop->logger, LOGGER_ERR, "Mirroring not initialized at SETUP, playing will fail!");
- http_response_set_disconnect(response, 1);
- }
-
- plist_t res_stream_node = plist_new_dict();
- plist_t res_stream_data_port_node = plist_new_uint(dport);
- plist_t res_stream_type_node = plist_new_uint(110);
- plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
- plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
- plist_array_append_item(res_streams_node, res_stream_node);
-
- break;
- } case 96: {
- // Audio
- unsigned short cport = conn->raop->control_lport, dport = conn->raop->data_lport;
- unsigned short remote_cport = 0;
- unsigned char ct = 0;
- unsigned int sr = AUDIO_SAMPLE_RATE; /* all AirPlay audio formats supported so far have sample rate 44.1kHz */
-
- uint64_t uint_val = 0;
- plist_t req_stream_control_port_node = plist_dict_get_item(req_stream_node, "controlPort");
- plist_get_uint_val(req_stream_control_port_node, &uint_val);
- remote_cport = (unsigned short) uint_val; /* must != 0 to activate audio resend requests */
-
- plist_t req_stream_ct_node = plist_dict_get_item(req_stream_node, "ct");
- plist_get_uint_val(req_stream_ct_node, &uint_val);
- ct = (unsigned char) uint_val;
-
- if (conn->raop->callbacks.audio_get_format) {
- /* get additional audio format parameters */
- uint64_t audioFormat = 0;
- unsigned short spf = 0;
- bool isMedia = false;
- bool usingScreen = false;
- uint8_t bool_val = 0;
-
- plist_t req_stream_spf_node = plist_dict_get_item(req_stream_node, "spf");
- plist_get_uint_val(req_stream_spf_node, &uint_val);
- spf = (unsigned short) uint_val;
-
- plist_t req_stream_audio_format_node = plist_dict_get_item(req_stream_node, "audioFormat");
- plist_get_uint_val(req_stream_audio_format_node, &audioFormat);
-
- plist_t req_stream_is_media_node = plist_dict_get_item(req_stream_node, "isMedia");
- if (req_stream_is_media_node) {
- plist_get_bool_val(req_stream_is_media_node, &bool_val);
- isMedia = (bool) bool_val;
- } else {
- isMedia = false;
- }
-
- plist_t req_stream_using_screen_node = plist_dict_get_item(req_stream_node, "usingScreen");
- if (req_stream_using_screen_node) {
- plist_get_bool_val(req_stream_using_screen_node, &bool_val);
- usingScreen = (bool) bool_val;
- } else {
- usingScreen = false;
- }
-
- conn->raop->callbacks.audio_get_format(conn->raop->callbacks.cls, &ct, &spf, &usingScreen, &isMedia, &audioFormat);
- }
-
- if (conn->raop_rtp) {
- raop_rtp_start_audio(conn->raop_rtp, &remote_cport, &cport, &dport, &ct, &sr);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "RAOP initialized success");
- } else {
- logger_log(conn->raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!");
- http_response_set_disconnect(response, 1);
- }
-
- plist_t res_stream_node = plist_new_dict();
- plist_t res_stream_data_port_node = plist_new_uint(dport);
- plist_t res_stream_control_port_node = plist_new_uint(cport);
- plist_t res_stream_type_node = plist_new_uint(96);
- plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
- plist_dict_set_item(res_stream_node, "controlPort", res_stream_control_port_node);
- plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
- plist_array_append_item(res_streams_node, res_stream_node);
-
- break;
+ if (conn->raop_rtp_mirror) {
+ raop_rtp_mirror_init_aes(conn->raop_rtp_mirror, &stream_connection_id);
+ raop_rtp_mirror_start(conn->raop_rtp_mirror, &dport, conn->raop->clientFPSdata);
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "Mirroring initialized successfully");
+ } else {
+ logger_log(conn->raop->logger, LOGGER_ERR, "Mirroring not initialized at SETUP, playing will fail!");
+ http_response_set_disconnect(response, 1);
}
- default:
- logger_log(conn->raop->logger, LOGGER_ERR, "SETUP tries to setup stream of unknown type %llu", type);
+ plist_t res_stream_node = plist_new_dict();
+ plist_t res_stream_data_port_node = plist_new_uint(dport);
+ plist_t res_stream_type_node = plist_new_uint(110);
+ plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
+ plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
+ plist_array_append_item(res_streams_node, res_stream_node);
+
+ break;
+ }
+ case 96: {
+ // Audio
+ unsigned short cport = conn->raop->control_lport, dport = conn->raop->data_lport;
+ unsigned short remote_cport = 0;
+ unsigned char ct = 0;
+ unsigned int sr = AUDIO_SAMPLE_RATE; /* all AirPlay audio formats supported so far have sample rate 44.1kHz */
+
+ uint64_t uint_val = 0;
+ plist_t req_stream_control_port_node = plist_dict_get_item(req_stream_node, "controlPort");
+ plist_get_uint_val(req_stream_control_port_node, &uint_val);
+ remote_cport = (unsigned short) uint_val; /* must != 0 to activate audio resend requests */
+
+ plist_t req_stream_ct_node = plist_dict_get_item(req_stream_node, "ct");
+ plist_get_uint_val(req_stream_ct_node, &uint_val);
+ ct = (unsigned char) uint_val;
+
+ if (conn->raop->callbacks.audio_get_format) {
+ /* get additional audio format parameters */
+ uint64_t audioFormat = 0;
+ unsigned short spf = 0;
+ bool isMedia = false;
+ bool usingScreen = false;
+ uint8_t bool_val = 0;
+
+ plist_t req_stream_spf_node = plist_dict_get_item(req_stream_node, "spf");
+ plist_get_uint_val(req_stream_spf_node, &uint_val);
+ spf = (unsigned short) uint_val;
+
+ plist_t req_stream_audio_format_node = plist_dict_get_item(req_stream_node, "audioFormat");
+ plist_get_uint_val(req_stream_audio_format_node, &audioFormat);
+
+ plist_t req_stream_is_media_node = plist_dict_get_item(req_stream_node, "isMedia");
+ if (req_stream_is_media_node) {
+ plist_get_bool_val(req_stream_is_media_node, &bool_val);
+ isMedia = (bool) bool_val;
+ } else {
+ isMedia = false;
+ }
+
+ plist_t req_stream_using_screen_node = plist_dict_get_item(req_stream_node, "usingScreen");
+ if (req_stream_using_screen_node) {
+ plist_get_bool_val(req_stream_using_screen_node, &bool_val);
+ usingScreen = (bool) bool_val;
+ } else {
+ usingScreen = false;
+ }
+
+ conn->raop->callbacks.audio_get_format(conn->raop->callbacks.cls, &ct, &spf, &usingScreen, &isMedia, &audioFormat);
+ }
+
+ if (conn->raop_rtp) {
+ raop_rtp_start_audio(conn->raop_rtp, &remote_cport, &cport, &dport, &ct, &sr);
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "RAOP initialized success");
+ } else {
+ logger_log(conn->raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!");
http_response_set_disconnect(response, 1);
- break;
+ }
+
+ plist_t res_stream_node = plist_new_dict();
+ plist_t res_stream_data_port_node = plist_new_uint(dport);
+ plist_t res_stream_control_port_node = plist_new_uint(cport);
+ plist_t res_stream_type_node = plist_new_uint(96);
+ plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node);
+ plist_dict_set_item(res_stream_node, "controlPort", res_stream_control_port_node);
+ plist_dict_set_item(res_stream_node, "type", res_stream_type_node);
+ plist_array_append_item(res_streams_node, res_stream_node);
+
+ break;
+ }
+
+ default:
+ logger_log(conn->raop->logger, LOGGER_ERR, "SETUP tries to setup stream of unknown type %llu", type);
+ http_response_set_disconnect(response, 1);
+ break;
}
}
@@ -1036,7 +1037,7 @@ raop_handler_get_parameter(raop_conn_t *conn,
char volume[25] = "volume: 0.0\r\n";
if (conn->raop->callbacks.audio_set_client_volume) {
snprintf(volume, 25, "volume: %9.6f\r\n", conn->raop->callbacks.audio_set_client_volume(conn->raop->callbacks.cls));
- }
+ }
http_response_add_header(response, "Content-Type", "text/parameters");
*response_data = strdup(volume);
if (*response_data) {
@@ -1045,9 +1046,11 @@ raop_handler_get_parameter(raop_conn_t *conn,
return;
}
- for (next = current ; (datalen - (next - data) > 0) ; ++next)
- if (*next == '\r')
+ for (next = current ; (datalen - (next - data) > 0) ; ++next) {
+ if (*next == '\r') {
break;
+ }
+ }
if ((datalen - (next - data) >= 2) && !strncmp(next, "\r\n", 2)) {
if ((next - current) > 0) {
@@ -1187,7 +1190,7 @@ raop_handler_teardown(raop_conn_t *conn,
if (val == 96) {
teardown_96 = true;
} else if (val == 110) {
- teardown_110 = true;
+ teardown_110 = true;
}
}
}
diff --git a/lib/raop_ntp.c b/lib/raop_ntp.c
index 2264883..7d9c225 100644
--- a/lib/raop_ntp.c
+++ b/lib/raop_ntp.c
@@ -352,7 +352,7 @@ raop_ntp_thread(void *arg)
(double) t2 / SECOND_IN_NSECS, str);
free(str);
}
- // The iOS client device sends its time in seconds relative to an arbitrary Epoch (the last boot).
+ // The iOS client device sends its time in seconds relative to an arbitrary Epoch (the last boot).
// For a little bonus confusion, they add SECONDS_FROM_1900_TO_1970.
// This means we have to expect some rather huge offset, but its growth or shrink over time should be small.
diff --git a/lib/raop_rtp.c b/lib/raop_rtp.c
index f51eeab..51c304e 100644
--- a/lib/raop_rtp.c
+++ b/lib/raop_rtp.c
@@ -195,7 +195,6 @@ raop_rtp_init(logger_t *logger, raop_callbacks_t *callbacks, raop_ntp_t *ntp, co
return raop_rtp;
}
-
void
raop_rtp_destroy(raop_rtp_t *raop_rtp)
{
@@ -515,7 +514,7 @@ raop_rtp_thread_udp(void *arg)
} else {
client_ntp_sync_prev = raop_rtp->client_ntp_sync;
rtp_sync_prev = raop_rtp->rtp_sync;
- }
+ }
raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
uint64_t sync_ntp_raw = byteutils_get_long_be(packet, 8);
raop_rtp->client_ntp_sync = raop_remote_timestamp_to_nano_seconds(raop_rtp->ntp, sync_ntp_raw);
@@ -564,20 +563,20 @@ raop_rtp_thread_udp(void *arg)
* three times; the secnum and rtp_timestamp increment according to the same pattern as
* AAC-ELD packets with audio content.*/
- /* When the ALAC audio stream starts, the initial packets are length-44 packets with
- * the same 32-byte encrypted payload which after decryption is the beginning of a
- * 32-byte ALAC packet, presumably with format information, but not actual audio data.
- * The secnum and rtp_timestamp in the packet header increment according to the same
- * pattern as ALAC packets with audio content */
+ /* When the ALAC audio stream starts, the initial packets are length-44 packets with
+ * the same 32-byte encrypted payload which after decryption is the beginning of a
+ * 32-byte ALAC packet, presumably with format information, but not actual audio data.
+ * The secnum and rtp_timestamp in the packet header increment according to the same
+ * pattern as ALAC packets with audio content */
- /* The first ALAC packet with data seems to be decoded just before the first sync event
- * so its dequeuing should be delayed until the first rtp sync has occurred */
+ /* The first ALAC packet with data seems to be decoded just before the first sync event
+ * so its dequeuing should be delayed until the first rtp sync has occurred */
- if (FD_ISSET(raop_rtp->dsock, &rfds)) {
- if (!raop_rtp->initial_sync && !video_arrival_offset) {
+ if (FD_ISSET(raop_rtp->dsock, &rfds)) {
+ if (!raop_rtp->initial_sync && !video_arrival_offset) {
video_arrival_offset = raop_ntp_get_video_arrival_offset(raop_rtp->ntp);
- }
+ }
//logger_log(raop_rtp->logger, LOGGER_INFO, "Would have data packet in queue");
// Receiving audio data here
saddrlen = sizeof(saddr);
@@ -594,17 +593,17 @@ raop_rtp_thread_udp(void *arg)
free (str);
}
continue;
- }
+ }
- if (!raop_rtp->initial_sync && raop_rtp->ct == 8 && video_arrival_offset) {
+ if (!raop_rtp->initial_sync && raop_rtp->ct == 8 && video_arrival_offset) {
/* estimate a fake initial remote timestamp for video synchronization with AAC audio before the first rtp sync */
- uint64_t ts = raop_ntp_get_local_time() - video_arrival_offset;
- double delay = DELAY_AAC;
- ts += (uint64_t) (delay * SEC);
- raop_rtp->client_ntp_sync = ts;
- raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
- raop_rtp->initial_sync = true;
- }
+ uint64_t ts = raop_ntp_get_local_time() - video_arrival_offset;
+ double delay = DELAY_AAC;
+ ts += (uint64_t) (delay * SEC);
+ raop_rtp->client_ntp_sync = ts;
+ raop_rtp->rtp_sync = byteutils_get_int_be(packet, 4);
+ raop_rtp->initial_sync = true;
+ }
if (packetlen == 16 && memcmp(packet + 12, no_data_marker, 4) == 0) {
/* this is a "no data" packet */
@@ -612,12 +611,12 @@ raop_rtp_thread_udp(void *arg)
continue;
}
- if (raop_rtp->ct == 2 && packetlen == 44) continue; /* ignore the ALAC packets with format information only. */
+ if (raop_rtp->ct == 2 && packetlen == 44) continue; /* ignore the ALAC packets with format information only. */
int result = raop_buffer_enqueue(raop_rtp->buffer, packet, packetlen, 1);
assert(result >= 0);
- if (!raop_rtp->initial_sync) {
+ if (!raop_rtp->initial_sync) {
/* wait until the first sync before dequeing ALAC */
continue;
} else {
diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c
index 82c55df..029c6d5 100644
--- a/lib/raop_rtp_mirror.c
+++ b/lib/raop_rtp_mirror.c
@@ -330,13 +330,13 @@ raop_rtp_mirror_thread(void *arg)
/*packet[0:3] contains the payload size */
int payload_size = byteutils_get_int(packet, 0);
char packet_description[13] = {0};
- char *p = packet_description;
+ char *p = packet_description;
int n = sizeof(packet_description);
- for (int i = 4; i < 8; i++) {
+ for (int i = 4; i < 8; i++) {
snprintf(p, n, "%2.2x ", (unsigned int) packet[i]);
n -= 3;
p += 3;
- }
+ }
ntp_timestamp_raw = byteutils_get_long(packet, 8);
ntp_timestamp_remote = raop_ntp_timestamp_to_nano_seconds(ntp_timestamp_raw, false);
if (first_packet) {
@@ -345,14 +345,14 @@ raop_rtp_mirror_thread(void *arg)
first_packet = false;
}
- /* packet[4] + packet[5] identify the payload type: values seen are: *
+ /* packet[4] + packet[5] identify the payload type: values seen are: *
* 0x00 0x00: encrypted packet containing a non-IDR type 1 VCL NAL unit *
* 0x00 0x10: encrypted packet containing an IDR type 5 VCL NAL unit *
* 0x01 0x00: unencrypted packet containing a type 7 SPS NAL + a type 8 PPS NAL unit *
* 0x02 0x00: unencrypted packet (old protocol) no payload, sent once every second *
* 0x05 0x00 unencrypted packet with a "streaming report", sent once per second. */
- /* packet[6] + packet[7] may list a payload "option": values seen are: *
+ /* packet[6] + packet[7] may list a payload "option": values seen are: *
* 0x00 0x00 : encrypted and "streaming report" packets *
* 0x1e 0x00 : old protocol (seen in AirMyPC) no-payload once-per-second packets *
* 0x16 0x01 : seen in most unencrypted h264 SPS+PPS packets *
@@ -395,7 +395,7 @@ raop_rtp_mirror_thread(void *arg)
break;
}
- switch (packet[4]) {
+ switch (packet[4]) {
case 0x00:
// Normal video data (VCL NAL)
@@ -415,7 +415,7 @@ raop_rtp_mirror_thread(void *arg)
}
unsigned char* payload_out;
- unsigned char* payload_decrypted;
+ unsigned char* payload_decrypted;
/*
* nal_types:1 Coded non-partitioned slice of a non-IDR picture
* 5 Coded non-partitioned slice of an IDR picture
@@ -437,7 +437,7 @@ raop_rtp_mirror_thread(void *arg)
"raop_rtp_mirror: prepended sps_pps timestamp does not match timestamp of "
"video payload\n%llu\n%llu , discarding", ntp_timestamp_raw, ntp_timestamp_nal);
free (sps_pps);
- sps_pps = NULL;
+ sps_pps = NULL;
prepend_sps_pps = false;
}
@@ -447,7 +447,7 @@ raop_rtp_mirror_thread(void *arg)
payload_decrypted = payload_out + sps_pps_len;
memcpy(payload_out, sps_pps, sps_pps_len);
free (sps_pps);
- sps_pps = NULL;
+ sps_pps = NULL;
} else {
payload_out = (unsigned char*) malloc(payload_size);
payload_decrypted = payload_out;
@@ -474,11 +474,11 @@ raop_rtp_mirror_thread(void *arg)
valid_data = false;
break;
}
- int nalu_type;
- if (h265_video) {
+ int nalu_type;
+ if (h265_video) {
nalu_type = payload_decrypted[nalu_size] & 0x7e >> 1;;
//logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG," h265 video, NALU type %d, size %d", nalu_type, nc_len);
- } else {
+ } else {
nalu_type = payload_decrypted[nalu_size] & 0x1f;
int ref_idc = (payload_decrypted[nalu_size] >> 5);
switch (nalu_type) {
@@ -486,7 +486,7 @@ raop_rtp_mirror_thread(void *arg)
case 5: /*IDR, slice_layer_without_partitioning */
case 1: /*non-IDR, slice_layer_without_partitioning */
break;
- case 2: /* slice data partition A */
+ case 2: /* slice data partition A */
case 3: /* slice data partition B */
case 4: /* slice data partition C */
logger_log(raop_rtp_mirror->logger, LOGGER_INFO,
@@ -526,9 +526,9 @@ raop_rtp_mirror_thread(void *arg)
"unexpected non-VCL NAL unit: nalu_type = %d, ref_idc = %d, nalu_size = %d,"
"processed bytes %d, payloadsize = %d nalus_count = %d",
nalu_type, ref_idc, nc_len, nalu_size, payload_size, nalus_count);
- break;
- }
- }
+ break;
+ }
+ }
nalu_size += nc_len;
}
if (nalu_size != payload_size) valid_data = false;
@@ -540,7 +540,7 @@ raop_rtp_mirror_thread(void *arg)
payload_decrypted = NULL;
video_decode_struct video_data;
- video_data.is_h265 = h265_video;
+ video_data.is_h265 = h265_video;
video_data.ntp_time_local = ntp_timestamp_local;
video_data.ntp_time_remote = ntp_timestamp_remote;
video_data.nal_count = nalus_count; /*nal_count will be the number of nal units in the packet */
@@ -571,13 +571,13 @@ raop_rtp_mirror_thread(void *arg)
bytes 56-59 width
bytes 60-63 height
bytes 64-127 all 0x0
- */
+ */
// The information in the payload contains an SPS and a PPS NAL
// The sps_pps is not encrypted
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived unencrypted codec packet from client:"
" payload_size %d header %s ts_client = %8.6f",
- payload_size, packet_description, (double) ntp_timestamp_remote / SEC);
+ payload_size, packet_description, (double) ntp_timestamp_remote / SEC);
if (packet[6] == 0x56 || packet[6] == 0x5e) {
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "This packet indicates video stream is stopping");
@@ -614,7 +614,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "raop_rtp_mirror width_source = %f height_source = %f width = %f height = %f",
width_source, height_source, width, height);
- if (payload_size == 0) {
+ if (payload_size == 0) {
logger_log(raop_rtp_mirror->logger, LOGGER_ERR, "raop_rtp_mirror: received type 0x01 packet with no payload:\n"
"this indicates non-h264 video but Airplay features bit 42 (SupportsScreenMultiCodec) is not set\n"
"use startup option \"-h265\" to set this bit and support h265 (4K) video");
@@ -625,7 +625,7 @@ raop_rtp_mirror_thread(void *arg)
free(sps_pps);
sps_pps = NULL;
}
- /* test for a H265 VPS/SPS/PPS */
+ /* test for a H265 VPS/SPS/PPS */
unsigned char hvc1[] = { 0x68, 0x76, 0x63, 0x31 };
if (!memcmp(payload + 4, hvc1, 4)) {
@@ -677,7 +677,7 @@ raop_rtp_mirror_thread(void *arg)
break;
}
sps_size = byteutils_get_short_be(ptr, 3);
- ptr += 5;
+ ptr += 5;
sps = ptr;
if (logger_debug) {
char *str = utils_data_to_string(sps, sps_size, 16);
@@ -690,12 +690,12 @@ raop_rtp_mirror_thread(void *arg)
raop_rtp_mirror->callbacks.video_pause(raop_rtp_mirror->callbacks.cls);
break;
}
- pps_size = byteutils_get_short_be(ptr, 3);
+ pps_size = byteutils_get_short_be(ptr, 3);
ptr += 5;
pps = ptr;
if (logger_debug) {
char *str = utils_data_to_string(pps, pps_size, 16);
- logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "h265 pps size %d\n%s",pps_size, str);
+ logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "h265 pps size %d\n%s",pps_size, str);
free(str);
}
@@ -802,7 +802,7 @@ raop_rtp_mirror_thread(void *arg)
int plist_size = payload_size;
if (payload_size > 25000) {
- plist_size = payload_size - 25000;
+ plist_size = payload_size - 25000;
if (logger_debug) {
char *str = utils_data_to_string(payload + plist_size, 16, 16);
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG,
@@ -848,7 +848,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "raop_rtp_mirror exiting TCP thread");
if (conn_reset&& raop_rtp_mirror->callbacks.conn_reset) {
- raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls, 1);
+ raop_rtp_mirror->callbacks.conn_reset(raop_rtp_mirror->callbacks.cls, 1);
}
if (unsupported_codec) {
From 32d5de23c8b0d6b8c61f01e43f48b9827daae7ca Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 8 Aug 2025 15:42:07 -0400
Subject: [PATCH 08/58] whitespace cleanup
---
lib/airplay_video.c | 25 ++++++------
lib/dnssd.c | 28 +++++++-------
lib/http_handlers.h | 93 ++++++++++++++++++++++-----------------------
lib/http_request.c | 20 +++++-----
lib/raop.c | 34 ++++++++---------
lib/utils.c | 10 ++---
6 files changed, 103 insertions(+), 107 deletions(-)
diff --git a/lib/airplay_video.c b/lib/airplay_video.c
index 9b0ccd2..dfcea64 100644
--- a/lib/airplay_video.c
+++ b/lib/airplay_video.c
@@ -58,7 +58,7 @@ int airplay_video_service_init(raop_t *raop, unsigned short http_port,
airplay_video_t *airplay_video = deregister_airplay_video(raop);
if (airplay_video) {
- airplay_video_service_destroy(airplay_video);
+ airplay_video_service_destroy(airplay_video);
}
/* calloc guarantees that the 36-character strings apple_session_id and
@@ -74,11 +74,11 @@ int airplay_video_service_init(raop_t *raop, unsigned short http_port,
snprintf(ptr, 6, "%-5u", http_port);
ptr = strstr(airplay_video->local_uri_prefix, " ");
if (ptr) {
- *ptr = '\0';
+ *ptr = '\0';
}
if (!register_airplay_video(raop, airplay_video)) {
- return -2;
+ return -2;
}
//printf(" %p %p\n", airplay_video, get_airplay_video(raop));
@@ -118,7 +118,6 @@ airplay_video_service_destroy(airplay_video_t *airplay_video)
free (airplay_video->master_playlist);
}
-
free (airplay_video);
}
@@ -146,19 +145,19 @@ const char *get_playback_uuid(airplay_video_t *airplay_video) {
}
void set_uri_prefix(airplay_video_t *airplay_video, char *uri_prefix, int uri_prefix_len) {
- if (airplay_video->uri_prefix) {
- free (airplay_video->uri_prefix);
- }
- airplay_video->uri_prefix = (char *) calloc(uri_prefix_len + 1, sizeof(char));
- memcpy(airplay_video->uri_prefix, uri_prefix, uri_prefix_len);
+ if (airplay_video->uri_prefix) {
+ free (airplay_video->uri_prefix);
+ }
+ airplay_video->uri_prefix = (char *) calloc(uri_prefix_len + 1, sizeof(char));
+ memcpy(airplay_video->uri_prefix, uri_prefix, uri_prefix_len);
}
char *get_uri_prefix(airplay_video_t *airplay_video) {
- return airplay_video->uri_prefix;
+ return airplay_video->uri_prefix;
}
char *get_uri_local_prefix(airplay_video_t *airplay_video) {
- return airplay_video->local_uri_prefix;
+ return airplay_video->local_uri_prefix;
}
char *get_master_uri(airplay_video_t *airplay_video) {
@@ -198,7 +197,7 @@ void destroy_media_data_store(airplay_video_t *airplay_video) {
media_item_t *media_data_store = airplay_video->media_data_store;
if (media_data_store) {
for (int i = 0; i < airplay_video->num_uri ; i ++ ) {
- if (media_data_store[i].uri) {
+ if (media_data_store[i].uri) {
free (media_data_store[i].uri);
}
if (media_data_store[i].playlist) {
@@ -336,7 +335,7 @@ char *adjust_master_playlist (char *fcup_response_data, int fcup_response_datale
while (ptr != NULL) {
counter++;
ptr++;
- ptr = strstr(ptr, uri_prefix);
+ ptr = strstr(ptr, uri_prefix);
}
size_t len = uri_local_prefix_len - uri_prefix_len;
diff --git a/lib/dnssd.c b/lib/dnssd.c
index b66563c..23d3143 100644
--- a/lib/dnssd.c
+++ b/lib/dnssd.c
@@ -310,16 +310,16 @@ dnssd_register_raop(dnssd_t *dnssd, unsigned short port)
case 2:
case 3:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
- dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
- break;
+ dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 4, "0x84");
+ break;
case 1:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("true"), "true");
- dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
- break;
+ dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", 3, "0x8c");
+ break;
default:
dnssd->TXTRecordSetValue(&dnssd->raop_record, "pw", strlen("false"), "false");
- dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
- break;
+ dnssd->TXTRecordSetValue(&dnssd->raop_record, "sf", strlen(RAOP_SF), RAOP_SF);
+ break;
}
dnssd->TXTRecordSetValue(&dnssd->raop_record, "sr", strlen(RAOP_SR), RAOP_SR);
dnssd->TXTRecordSetValue(&dnssd->raop_record, "ss", strlen(RAOP_SS), RAOP_SS);
@@ -382,8 +382,8 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
switch (dnssd->pin_pw) {
case 1: // display onscreen pin
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
- dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
- break;
+ dnssd->TXTRecordSetValue(&dnssd->airplay_record, "flags", 3, "0x4");
+ break;
case 2: // require password
case 3:
dnssd->TXTRecordSetValue(&dnssd->airplay_record, "pw", strlen("true"), "true");
@@ -476,13 +476,13 @@ dnssd_unregister_airplay(dnssd_t *dnssd)
}
uint64_t dnssd_get_airplay_features(dnssd_t *dnssd) {
- uint64_t features = ((uint64_t) dnssd->features2) << 32;
- features += (uint64_t) dnssd->features1;
- return features;
+ uint64_t features = ((uint64_t) dnssd->features2) << 32;
+ features += (uint64_t) dnssd->features1;
+ return features;
}
void dnssd_set_pk(dnssd_t *dnssd, char * pk_str) {
- dnssd->pk = pk_str;
+ dnssd->pk = pk_str;
}
void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
@@ -498,8 +498,8 @@ void dnssd_set_airplay_features(dnssd_t *dnssd, int bit, int val) {
features = &(dnssd->features1);
}
if (val) {
- *features = *features | mask;
+ *features = *features | mask;
} else {
- *features = *features & ~mask;
+ *features = *features & ~mask;
}
}
diff --git a/lib/http_handlers.h b/lib/http_handlers.h
index 25c83d9..1c17717 100644
--- a/lib/http_handlers.h
+++ b/lib/http_handlers.h
@@ -124,7 +124,7 @@ http_handler_rate(raop_conn_t *conn, http_request_t *request, http_response_t *r
if (end && end != rate) {
rate_value = value;
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_rate: got rate = %.6f", rate_value);
- }
+ }
}
conn->raop->callbacks.on_video_rate(conn->raop->callbacks.cls, rate_value);
}
@@ -142,7 +142,7 @@ http_handler_stop(raop_conn_t *conn, http_request_t *request, http_response_t *r
static void
http_handler_set_property(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
- char **response_data, int *response_datalen) {
+ char **response_data, int *response_datalen) {
const char *url = http_request_get_url(request);
const char *property = url + strlen("/setProperty?");
@@ -247,12 +247,12 @@ int create_playback_info_plist_xml(playback_info_t *playback_info, char **plist_
plist_t loaded_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->loadedTimeRanges, playback_info->num_loaded_time_ranges,
- loaded_time_ranges_node);
+ loaded_time_ranges_node);
plist_dict_set_item(res_root_node, "loadedTimeRanges", loaded_time_ranges_node);
plist_t seekable_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->seekableTimeRanges, playback_info->num_seekable_time_ranges,
- seekable_time_ranges_node);
+ seekable_time_ranges_node);
plist_dict_set_item(res_root_node, "seekableTimeRanges", seekable_time_ranges_node);
int len;
@@ -264,7 +264,6 @@ int create_playback_info_plist_xml(playback_info_t *playback_info, char **plist_
return len;
}
-
/* this handles requests from the Client for "Playback information" while the Media is playing on the
Media Player. (The Server gets this information by monitoring the Media Player). The Client could use
the information to e.g. update the slider it shows with progress to the player (0%-100%).
@@ -287,11 +286,11 @@ http_handler_playback_info(raop_conn_t *conn, http_request_t *request, http_resp
conn->raop->callbacks.on_video_acquire_playback_info(conn->raop->callbacks.cls, &playback_info);
if (playback_info.duration == -1.0) {
/* video has finished, reset */
- logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available (finishing)");
- //httpd_remove_known_connections(conn->raop->httpd);
- http_response_set_disconnect(response,1);
- conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
- return;
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available (finishing)");
+ //httpd_remove_known_connections(conn->raop->httpd);
+ http_response_set_disconnect(response,1);
+ conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
+ return;
} else if (playback_info.position == -1.0) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available");
return;
@@ -345,9 +344,8 @@ http_handler_reverse(raop_conn_t *conn, http_request_t *request, http_response_t
if (type_PTTH == 1) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "will use socket %d for %s connections", socket_fd, purpose);
http_response_init(response, "HTTP/1.1", 101, "Switching Protocols");
- http_response_add_header(response, "Connection", "Upgrade");
- http_response_add_header(response, "Upgrade", "PTTH/1.0");
-
+ http_response_add_header(response, "Connection", "Upgrade");
+ http_response_add_header(response, "Upgrade", "PTTH/1.0");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "multiple TPPH connections (%d) are forbidden", type_PTTH );
}
@@ -368,7 +366,6 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
int request_id = 0;
int fcup_response_statuscode = 0;
bool logger_debug = (logger_get_level(conn->raop->logger) >= LOGGER_DEBUG);
-
const char* session_id = http_request_get_header(request, "X-Apple-Session-ID");
if (!session_id) {
@@ -405,7 +402,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
/* determine type of data */
plist_t req_type_node = plist_dict_get_item(req_root_node, "type");
if (!PLIST_IS_STRING(req_type_node)) {
- goto post_action_error;
+ goto post_action_error;
}
plist_t req_params_node = NULL;
@@ -431,15 +428,15 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
}
plist_t req_params_item_uuid_node = plist_dict_get_item(req_params_item_node, "uuid");
char* remove_uuid = NULL;
- plist_get_string_val(req_params_item_uuid_node, &remove_uuid);
+ plist_get_string_val(req_params_item_uuid_node, &remove_uuid);
const char *playback_uuid = get_playback_uuid(conn->raop->airplay_video);
- if (strcmp(remove_uuid, playback_uuid)) {
+ if (strcmp(remove_uuid, playback_uuid)) {
logger_log(conn->raop->logger, LOGGER_ERR, "uuid of playlist removal action request did not match current playlist:\n"
" current: %s\n remove: %s", playback_uuid, remove_uuid);
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "removal_uuid matches playback_uuid\n");
- }
- free (remove_uuid);
+ }
+ free (remove_uuid);
}
logger_log(conn->raop->logger, LOGGER_ERR, "FIXME: playlist removal not yet implemented");
goto finish;
@@ -531,11 +528,11 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (logger_debug) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response datalen = %d", fcup_response_datalen);
- char *data = malloc(fcup_response_datalen + 1);
- memcpy(data, fcup_response_data, fcup_response_datalen);
- data[fcup_response_datalen] = '\0';
- logger_log(conn->raop->logger, LOGGER_DEBUG, "begin FCUP Response data:\n%s\nend FCUP Response data",data);
- free (data);
+ char *data = malloc(fcup_response_datalen + 1);
+ memcpy(data, fcup_response_data, fcup_response_datalen);
+ data[fcup_response_datalen] = '\0';
+ logger_log(conn->raop->logger, LOGGER_DEBUG, "begin FCUP Response data:\n%s\nend FCUP Response data",data);
+ free (data);
}
@@ -543,24 +540,24 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (ptr) {
/* this is a master playlist */
char *uri_prefix = get_uri_prefix(conn->raop->airplay_video);
- char ** media_data_store = NULL;
+ char ** media_data_store = NULL;
int num_uri = 0;
char *uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
char *new_master = adjust_master_playlist (fcup_response_data, fcup_response_datalen, uri_prefix, uri_local_prefix);
store_master_playlist(conn->raop->airplay_video, new_master);
create_media_uri_table(uri_prefix, fcup_response_data, fcup_response_datalen, &media_data_store, &num_uri);
- create_media_data_store(conn->raop->airplay_video, media_data_store, num_uri);
- num_uri = get_num_media_uri(conn->raop->airplay_video);
- set_next_media_uri_id(conn->raop->airplay_video, 0);
+ create_media_data_store(conn->raop->airplay_video, media_data_store, num_uri);
+ num_uri = get_num_media_uri(conn->raop->airplay_video);
+ set_next_media_uri_id(conn->raop->airplay_video, 0);
} else {
/* this is a media playlist */
assert(fcup_response_data);
- char *playlist = (char *) calloc(fcup_response_datalen + 1, sizeof(char));
- memcpy(playlist, fcup_response_data, fcup_response_datalen);
+ char *playlist = (char *) calloc(fcup_response_datalen + 1, sizeof(char));
+ memcpy(playlist, fcup_response_data, fcup_response_datalen);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
- --uri_num; // (next num is current num + 1)
- store_media_playlist(conn->raop->airplay_video, playlist, uri_num);
+ --uri_num; // (next num is current num + 1)
+ store_media_playlist(conn->raop->airplay_video, playlist, uri_num);
float duration = 0.0f;
int count = analyze_media_playlist(playlist, &duration);
if (count) {
@@ -583,7 +580,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
fcup_request((void *) conn, get_media_uri_by_num(conn->raop->airplay_video, uri_num),
apple_session_id,
get_next_FCUP_RequestID(conn->raop->airplay_video));
- set_next_media_uri_id(conn->raop->airplay_video, ++uri_num);
+ set_next_media_uri_id(conn->raop->airplay_video, ++uri_num);
} else {
char * uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
conn->raop->callbacks.on_video_play(conn->raop->callbacks.cls,
@@ -599,7 +596,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
http_response_init(response, "HTTP/1.1", 400, "Bad Request");
if (req_root_node) {
- plist_free(req_root_node);
+ plist_free(req_root_node);
}
}
@@ -643,23 +640,23 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
logger_log(conn->raop->logger, LOGGER_DEBUG, "request header:\n%s", header_str);
- data_is_binary_plist = (strstr(header_str, "x-apple-binary-plist") != NULL);
- data_is_text = (strstr(header_str, "text/parameters") != NULL);
- data_is_octet = (strstr(header_str, "octet-stream") != NULL);
- free (header_str);
+ data_is_binary_plist = (strstr(header_str, "x-apple-binary-plist") != NULL);
+ data_is_text = (strstr(header_str, "text/parameters") != NULL);
+ data_is_octet = (strstr(header_str, "octet-stream") != NULL);
+ free (header_str);
}
if (!data_is_text && !data_is_octet && !data_is_binary_plist) {
- goto play_error;
+ goto play_error;
}
if (data_is_text) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is text (unsupported)");
- goto play_error;
+ goto play_error;
}
if (data_is_octet) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is octet-stream (unsupported)");
- goto play_error;
+ goto play_error;
}
if (data_is_binary_plist) {
@@ -671,9 +668,9 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
} else {
char* playback_uuid = NULL;
plist_get_string_val(req_uuid_node, &playback_uuid);
- set_playback_uuid(conn->raop->airplay_video, playback_uuid);
+ set_playback_uuid(conn->raop->airplay_video, playback_uuid);
free (playback_uuid);
- }
+ }
plist_t req_content_location_node = plist_dict_get_item(req_root_node, "Content-Location");
if (!req_content_location_node) {
@@ -696,18 +693,18 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
plist_t req_start_position_seconds_node = plist_dict_get_item(req_root_node, "Start-Position-Seconds");
if (!req_start_position_seconds_node) {
logger_log(conn->raop->logger, LOGGER_INFO, "No Start-Position-Seconds in Play request");
- } else {
+ } else {
double start_position = 0.0;
plist_get_real_val(req_start_position_seconds_node, &start_position);
- start_position_seconds = (float) start_position;
+ start_position_seconds = (float) start_position;
}
- set_start_position_seconds(conn->raop->airplay_video, (float) start_position_seconds);
+ set_start_position_seconds(conn->raop->airplay_video, (float) start_position_seconds);
}
char *ptr = strstr(playback_location, "/master.m3u8");
if (!ptr) {
logger_log(conn->raop->logger, LOGGER_ERR, "Content-Location has unsupported form:\n%s\n", playback_location);
- goto play_error;
+ goto play_error;
}
int prefix_len = (int) (ptr - playback_location);
set_uri_prefix(conn->raop->airplay_video, playback_location, prefix_len);
@@ -760,7 +757,7 @@ http_handler_hls(raop_conn_t *conn, http_request_t *request, http_response_t *r
if (!strcmp(url, "/master.m3u8")){
char * master_playlist = get_master_playlist(conn->raop->airplay_video);
- if (master_playlist) {
+ if (master_playlist) {
size_t len = strlen(master_playlist);
char * data = (char *) malloc(len + 1);
memcpy(data, master_playlist, len);
diff --git a/lib/http_request.c b/lib/http_request.c
index 76bece6..d7e800e 100644
--- a/lib/http_request.c
+++ b/lib/http_request.c
@@ -81,11 +81,11 @@ on_header_field(llhttp_t *parser, const char *at, size_t length)
/* Allocate space in the current header string */
if (request->headers[request->headers_index] == NULL) {
- request->headers[request->headers_index] = calloc(1, length+1);
+ request->headers[request->headers_index] = calloc(1, length + 1);
} else {
request->headers[request->headers_index] = realloc(
request->headers[request->headers_index],
- strlen(request->headers[request->headers_index])+length+1
+ strlen(request->headers[request->headers_index]) + length + 1
);
}
assert(request->headers[request->headers_index]);
@@ -106,11 +106,11 @@ on_header_value(llhttp_t *parser, const char *at, size_t length)
/* Allocate space in the current header string */
if (request->headers[request->headers_index] == NULL) {
- request->headers[request->headers_index] = calloc(1, length+1);
+ request->headers[request->headers_index] = calloc(1, length + 1);
} else {
request->headers[request->headers_index] = realloc(
request->headers[request->headers_index],
- strlen(request->headers[request->headers_index])+length+1
+ strlen(request->headers[request->headers_index]) + length + 1
);
}
assert(request->headers[request->headers_index]);
@@ -124,7 +124,7 @@ on_body(llhttp_t *parser, const char *at, size_t length)
{
http_request_t *request = parser->data;
- request->data = realloc(request->data, request->datalen+length);
+ request->data = realloc(request->data, request->datalen + length);
assert(request->data);
memcpy(request->data+request->datalen, at, length);
@@ -172,7 +172,7 @@ http_request_destroy(http_request_t *request)
if (request) {
free(request->url);
- for (i=0; iheaders_size; i++) {
+ for (i = 0; i < request->headers_size; i++) {
free(request->headers[i]);
}
free(request->headers);
@@ -273,7 +273,7 @@ http_request_get_header(http_request_t *request, const char *name)
return NULL;
}
- for (i=0; iheaders_size; i+=2) {
+ for (i = 0; i < request->headers_size; i += 2) {
if (!strcmp(request->headers[i], name)) {
return request->headers[i+1];
}
@@ -305,7 +305,7 @@ http_request_get_header_string(http_request_t *request, char **header_str)
int len = 0;
for (int i = 0; i < request->headers_size; i++) {
len += strlen(request->headers[i]);
- if (i%2 == 0) {
+ if (i % 2 == 0) {
len += 2;
} else {
len++;
@@ -321,12 +321,12 @@ http_request_get_header_string(http_request_t *request, char **header_str)
snprintf(p, n, "%s", request->headers[i]);
n -= hlen;
p += hlen;
- if (i%2 == 0) {
+ if (i % 2 == 0) {
snprintf(p, n, ": ");
n -= 2;
p += 2;
} else {
- snprintf(p, n, "\n");
+ snprintf(p, n, "\n");
n--;
p++;
}
diff --git a/lib/raop.c b/lib/raop.c
index d9939ba..95700f6 100644
--- a/lib/raop.c
+++ b/lib/raop.c
@@ -225,8 +225,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
logger_log(conn->raop->logger, LOGGER_INFO, "\"nohold\" feature: switch to new connection request from %s", ipaddr);
if (conn->raop->callbacks.video_reset) {
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
- }
- httpd_remove_known_connections(conn->raop->httpd);
+ }
+ httpd_remove_known_connections(conn->raop->httpd);
} else {
logger_log(conn->raop->logger, LOGGER_WARNING, "rejecting new connection request from %s", ipaddr);
*response = http_response_create();
@@ -245,7 +245,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
conn->client_session_id = (char *) malloc(len);
strncpy(conn->client_session_id, client_session_id, len);
/* airplay video has been requested: shut down any running RAOP udp services */
- raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
+ raop_conn_t *raop_conn = (raop_conn_t *) httpd_get_connection_by_type(conn->raop->httpd, CONNECTION_TYPE_RAOP, 1);
if (raop_conn) {
raop_rtp_mirror_t *raop_rtp_mirror = raop_conn->raop_rtp_mirror;
if (raop_rtp_mirror) {
@@ -273,7 +273,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
httpd_set_connection_type(conn->raop->httpd, ptr, CONNECTION_TYPE_HLS);
conn->connection_type = CONNECTION_TYPE_HLS;
} else {
- logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
+ logger_log(conn->raop->logger, LOGGER_WARNING, "connection from unknown connection type");
}
}
@@ -310,9 +310,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (request_data && logger_debug) {
if (request_datalen > 0) {
/* logger has a buffer limit of 4096 */
- if (data_is_plist) {
- plist_t req_root_node = NULL;
- plist_from_bin(request_data, request_datalen, &req_root_node);
+ if (data_is_plist) {
+ plist_t req_root_node = NULL;
+ plist_from_bin(request_data, request_datalen, &req_root_node);
char * plist_xml = NULL;
char * stripped_xml = NULL;
uint32_t plist_len;
@@ -353,7 +353,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (!strcmp(method, "POST")) {
if (!strcmp(url, "/feedback")) {
handler = &raop_handler_feedback;
- } else if (!strcmp(url, "/pair-pin-start")) {
+ } else if (!strcmp(url, "/pair-pin-start")) {
handler = &raop_handler_pairpinstart;
} else if (!strcmp(url, "/pair-setup-pin")) {
handler = &raop_handler_pairsetup_pin;
@@ -388,7 +388,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &raop_handler_teardown;
} else {
http_response_init(*response, protocol, 501, "Not Implemented");
- }
+ }
} else if (!hls_request && !strcmp(protocol, "HTTP/1.1")) {
if (!strcmp(method, "POST")) {
if (!strcmp(url, "/reverse")) {
@@ -415,9 +415,9 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &http_handler_playback_info;
}
} else if (!strcmp(method, "PUT")) {
- if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
+ if (!strncmp (url, "/setProperty?", strlen("/setProperty?"))) {
handler = &http_handler_set_property;
- }
+ }
}
} else if (hls_request) {
handler = &http_handler_hls;
@@ -426,8 +426,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (handler != NULL) {
handler(conn, request, *response, &response_data, &response_datalen);
} else {
- logger_log(conn->raop->logger, LOGGER_INFO,
- "Unhandled Client Request: %s %s %s", method, url, protocol);
+ logger_log(conn->raop->logger, LOGGER_INFO,
+ "Unhandled Client Request: %s %s %s", method, url, protocol);
}
finish:;
@@ -435,7 +435,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
http_response_add_header(*response, "Server", "AirTunes/"GLOBAL_VERSION);
if (cseq) {
http_response_add_header(*response, "CSeq", cseq);
- }
+ }
}
http_response_finish(*response, response_data, response_datalen);
@@ -488,7 +488,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
}
if (response_data) {
free(response_data);
- }
+ }
}
}
@@ -646,10 +646,10 @@ raop_destroy(raop_t *raop) {
pairing_destroy(raop->pairing);
httpd_destroy(raop->httpd);
logger_destroy(raop->logger);
- if (raop->nonce) {
+ if (raop->nonce) {
free(raop->nonce);
}
- if (raop->random_pw) {
+ if (raop->random_pw) {
free(raop->random_pw);
}
diff --git a/lib/utils.c b/lib/utils.c
index ce2623e..f354b66 100644
--- a/lib/utils.c
+++ b/lib/utils.c
@@ -312,20 +312,20 @@ char *utils_strip_data_from_plist_xml(char *plist_xml) {
nchars = eol + 1 - ptr1;
memcpy(ptr2, ptr1, nchars);
ptr2 += nchars;
- ptr1 += nchars;
+ ptr1 += nchars;
end = strstr(ptr1, "");
- assert(end);
+ assert(end);
count = 0;
do {
eol_data = eol;
eol = strchr(eol + 1, '\n');
count++;
} while (eol < end);
- count--; // last '\n' counted ends the first non-data line (contains "")
- if (count > 1) {
+ count--; // last '\n' counted ends the first non-data line (contains "")
+ if (count > 1) {
snprintf(line, sizeof(line), " (%d lines data omitted, 64 chars/line)\n", count);
nchars = strlen(line);
- memcpy(ptr2, line, nchars);
+ memcpy(ptr2, line, nchars);
ptr2 += nchars;
ptr1 = eol_data + 1;
} else {
From b5e083b1071afb7e2a80deac433160fe49952c4d Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 8 Aug 2025 17:06:14 -0400
Subject: [PATCH 09/58] whitespace cleanup
---
renderers/audio_renderer.c | 24 ++++++++--------
renderers/video_renderer.c | 58 +++++++++++++++++++-------------------
2 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/renderers/audio_renderer.c b/renderers/audio_renderer.c
index 5075018..9a669d3 100644
--- a/renderers/audio_renderer.c
+++ b/renderers/audio_renderer.c
@@ -103,15 +103,15 @@ static gboolean check_plugin_feature (const gchar *needed_feature)
plugin_feature = gst_registry_find_feature (registry, needed_feature, GST_TYPE_ELEMENT_FACTORY);
if (!plugin_feature) {
- g_print ("Required gstreamer libav plugin feature '%s' not found:\n\n"
- "This may be missing because the FFmpeg package used by GStreamer-1.x-libav is incomplete.\n"
- "(Some distributions provide an incomplete FFmpeg due to License or Patent issues:\n"
- "in such cases a complete version for that distribution is usually made available elsewhere)\n",
- needed_feature);
- ret = FALSE;
+ g_print ("Required gstreamer libav plugin feature '%s' not found:\n\n"
+ "This may be missing because the FFmpeg package used by GStreamer-1.x-libav is incomplete.\n"
+ "(Some distributions provide an incomplete FFmpeg due to License or Patent issues:\n"
+ "in such cases a complete version for that distribution is usually made available elsewhere)\n",
+ needed_feature);
+ ret = FALSE;
} else {
- gst_object_unref (plugin_feature);
- plugin_feature = NULL;
+ gst_object_unref (plugin_feature);
+ plugin_feature = NULL;
}
if (ret == FALSE) {
g_print ("\nif the plugin feature is installed, but not found, your gstreamer registry may have been corrupted.\n"
@@ -187,7 +187,7 @@ void audio_renderer_init(logger_t *render_logger, const char* audiosink, const b
g_assert (renderer_type[i]->pipeline);
gst_pipeline_use_clock(GST_PIPELINE_CAST(renderer_type[i]->pipeline), clock);
- renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
+ renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
renderer_type[i]->appsrc = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "audio_source");
renderer_type[i]->volume = gst_bin_get_by_name (GST_BIN (renderer_type[i]->pipeline), "volume");
switch (i) {
@@ -369,12 +369,12 @@ void audio_renderer_destroy() {
audio_renderer_stop();
for (int i = 0; i < NFORMATS ; i++ ) {
gst_object_unref (renderer_type[i]->bus);
- renderer_type[i]->bus = NULL;
+ renderer_type[i]->bus = NULL;
gst_object_unref (renderer_type[i]->volume);
- renderer_type[i]->volume = NULL;
+ renderer_type[i]->volume = NULL;
gst_object_unref (renderer_type[i]->appsrc);
renderer_type[i]->appsrc = NULL;
- gst_object_unref (renderer_type[i]->pipeline);
+ gst_object_unref (renderer_type[i]->pipeline);
renderer_type[i]->pipeline = NULL;
free(renderer_type[i]);
}
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 4086312..979c53f 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -211,8 +211,8 @@ GstElement *make_video_sink(const char *videosink, const char *videosink_options
pval++;
const gchar *property_name = (const gchar *) token;
const gchar *value = (const gchar *) pval;
- g_print("playbin_videosink property: \"%s\" \"%s\"\n", property_name, value);
- gst_util_set_object_arg(G_OBJECT (video_sink), property_name, value);
+ g_print("playbin_videosink property: \"%s\" \"%s\"\n", property_name, value);
+ gst_util_set_object_arg(G_OBJECT (video_sink), property_name, value);
}
}
free(options);
@@ -283,7 +283,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
}
logger_log(logger, LOGGER_INFO, "Will use GStreamer playbin version %u to play HLS streamed video", playbin_version);
g_assert(renderer_type[i]->pipeline);
- renderer_type[i]->codec = hls;
+ renderer_type[i]->codec = hls;
/* if we are not using an autovideosink, build a videosink based on the string "videosink" */
if (!auto_videosink) {
GstElement *playbin_videosink = make_video_sink(videosink, videosink_options);
@@ -297,9 +297,9 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
gint flags;
g_object_get(renderer_type[i]->pipeline, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_DOWNLOAD;
- flags |= GST_PLAY_FLAG_BUFFERING; // set by default in playbin3, but not in playbin2; is it needed?
+ flags |= GST_PLAY_FLAG_BUFFERING; // set by default in playbin3, but not in playbin2; is it needed?
g_object_set(renderer_type[i]->pipeline, "flags", flags, NULL);
- g_object_set (G_OBJECT (renderer_type[i]->pipeline), "uri", uri, NULL);
+ g_object_set (G_OBJECT (renderer_type[i]->pipeline), "uri", uri, NULL);
} else {
bool jpeg_pipeline = false;
switch (i) {
@@ -320,9 +320,9 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
g_assert(0);
}
GString *launch = g_string_new("appsrc name=video_source ! ");
- if (jpeg_pipeline) {
+ if (jpeg_pipeline) {
g_string_append(launch, "jpegdec ");
- } else {
+ } else {
g_string_append(launch, "queue ! ");
g_string_append(launch, parser);
g_string_append(launch, " ! ");
@@ -370,7 +370,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
logger_log(logger, LOGGER_ERR, "GStreamer gst_parse_launch failed to create video pipeline %d\n"
"*** error message from gst_parse_launch was:\n%s\n"
"launch string parsed was \n[%s]", i + 1, error->message, launch->str);
- if (strstr(error->message, "no element")) {
+ if (strstr(error->message, "no element")) {
logger_log(logger, LOGGER_ERR, "This error usually means that a uxplay option was mistyped\n"
" or some requested part of GStreamer is not installed\n");
}
@@ -385,7 +385,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
g_object_set(renderer_type[i]->appsrc, "caps", caps, "stream-type", 0, "is-live", TRUE, "format", GST_FORMAT_TIME, NULL);
g_string_free(launch, TRUE);
gst_caps_unref(caps);
- gst_object_unref(clock);
+ gst_object_unref(clock);
}
#ifdef X_DISPLAY_FIX
use_x11 = (strstr(videosink, "xvimagesink") || strstr(videosink, "ximagesink") || auto_videosink);
@@ -704,9 +704,9 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
g_print("GStreamer %s bus message %s %s %s %s\n", renderer_type[type]->codec,
GST_MESSAGE_SRC_NAME(message), GST_MESSAGE_TYPE_NAME(message), old_state_name, new_state_name);
}
- if (name) {
- g_free(name);
- }
+ if (name) {
+ g_free(name);
+ }
}
/* monitor hls video position until seek to hls_start_position is achieved */
@@ -716,21 +716,21 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
if (!GST_CLOCK_TIME_IS_VALID(hls_duration)) {
gst_element_query_duration (renderer->pipeline, GST_FORMAT_TIME, &hls_duration);
}
- gst_element_query_position (renderer_type[type]->pipeline, GST_FORMAT_TIME, &pos);
+ gst_element_query_position (renderer_type[type]->pipeline, GST_FORMAT_TIME, &pos);
//g_print("HLS position %" GST_TIME_FORMAT " requested_start_position %" GST_TIME_FORMAT " duration %" GST_TIME_FORMAT " %s\n",
// GST_TIME_ARGS(pos), GST_TIME_ARGS(hls_requested_start_position), GST_TIME_ARGS(hls_duration),
// (hls_seek_enabled ? "seek enabled" : "seek not enabled"));
if (pos > hls_requested_start_position) {
hls_requested_start_position = 0;
}
- if ( hls_requested_start_position && pos < hls_requested_start_position && hls_seek_enabled) {
+ if ( hls_requested_start_position && pos < hls_requested_start_position && hls_seek_enabled) {
g_print("***************** seek to hls_requested_start_position %" GST_TIME_FORMAT "\n", GST_TIME_ARGS(hls_requested_start_position));
if (gst_element_seek_simple (renderer_type[type]->pipeline, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, hls_requested_start_position)) {
hls_requested_start_position = 0;
}
- }
- }
+ }
+ }
}
switch (GST_MESSAGE_TYPE (message)) {
@@ -742,8 +742,8 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
gint percent = -1;
gst_message_parse_buffering(message, &percent);
hls_buffer_empty = TRUE;
- hls_buffer_full = FALSE;
- if (percent > 0) {
+ hls_buffer_full = FALSE;
+ if (percent > 0) {
hls_buffer_empty = FALSE;
renderer_type[type]->buffering_level = percent;
logger_log(logger, LOGGER_DEBUG, "Buffering :%d percent done", percent);
@@ -772,11 +772,11 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
"*** to select a videosink of your choice (see \"man uxplay\").\n\n"
"*** Raspberry Pi models 4B and earlier using Video4Linux2 may need \"-bt709\" uxplay option");
}
- g_error_free (err);
+ g_error_free (err);
g_free (debug);
- if (renderer_type[type]->appsrc) {
+ if (renderer_type[type]->appsrc) {
gst_app_src_end_of_stream (GST_APP_SRC(renderer_type[type]->appsrc));
- }
+ }
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
renderer_type[type]->terminate = TRUE;
@@ -789,7 +789,7 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
if (hls_video) {
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
- renderer_type[type]->terminate = TRUE;
+ renderer_type[type]->terminate = TRUE;
g_main_loop_quit( (GMainLoop *) loop);
}
break;
@@ -824,14 +824,14 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
char *sink = strstr(GST_MESSAGE_SRC_NAME(message), "-actual-sink-");
if (sink) {
sink += strlen("-actual-sink-");
- if (strstr(GST_MESSAGE_SRC_NAME(message), renderer_type[type]->codec)) {
+ if (strstr(GST_MESSAGE_SRC_NAME(message), renderer_type[type]->codec)) {
logger_log(logger, LOGGER_DEBUG, "GStreamer: automatically-selected videosink"
" (renderer %d: %s) is \"%ssink\"", renderer_type[type]->id + 1,
renderer_type[type]->codec, sink);
#ifdef X_DISPLAY_FIX
renderer_type[type]->use_x11 = (strstr(sink, "ximage") || strstr(sink, "xvimage"));
#endif
- renderer_type[type]->autovideo = false;
+ renderer_type[type]->autovideo = false;
}
}
}
@@ -889,7 +889,7 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
if (video_is_h265) {
logger_log(logger, LOGGER_ERR, "video is h265 but the -h265 option was not used");
return -1;
- }
+ }
renderer_used = renderer_type[1];
} else {
renderer_used = video_is_h265 ? renderer_type[2] : renderer_type[1];
@@ -906,7 +906,7 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
GstState old_state, new_state;
if (gst_element_get_state(renderer->pipeline, &old_state, &new_state, 100 * GST_MSECOND) == GST_STATE_CHANGE_FAILURE) {
g_error("video pipeline failed to go into playing state");
- return -1;
+ return -1;
}
logger_log(logger, LOGGER_DEBUG, "video_pipeline state change from %s to %s\n",
gst_element_state_get_name (old_state),gst_element_state_get_name (new_state));
@@ -932,12 +932,12 @@ unsigned int video_reset_callback(void * loop) {
if (video_terminate) {
video_terminate = false;
if (renderer->appsrc) {
- gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
+ gst_app_src_end_of_stream (GST_APP_SRC(renderer->appsrc));
}
gboolean flushing = TRUE;
gst_bus_set_flushing(renderer->bus, flushing);
- gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
- g_main_loop_quit( (GMainLoop *) loop);
+ gst_element_set_state (renderer->pipeline, GST_STATE_NULL);
+ g_main_loop_quit( (GMainLoop *) loop);
}
return (unsigned int) TRUE;
}
From 250b8645f875e7b39192ed985e58e287f905b55c Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 8 Aug 2025 18:09:25 -0400
Subject: [PATCH 10/58] whitespace uxplay.cpp
---
uxplay.cpp | 172 ++++++++++++++++++++++++++---------------------------
1 file changed, 84 insertions(+), 88 deletions(-)
diff --git a/uxplay.cpp b/uxplay.cpp
index c276103..4a9abe4 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -372,7 +372,7 @@ static void dump_video_to_file(unsigned char *data, int datalen) {
video_dumpfile_count++;
snprintf(suffix, sizeof(suffix), ".%d", video_dumpfile_count);
fn.append(suffix);
- }
+ }
fn.append(".h264");
video_dumpfile = fopen (fn.c_str(),"w");
if (video_dumpfile == NULL) {
@@ -848,17 +848,17 @@ static bool get_ports (int nports, std::string option, const char * value, unsig
static bool get_videoflip (const char *str, videoflip_t *videoflip) {
if (strlen(str) > 1) return false;
switch (str[0]) {
- case 'I':
- *videoflip = INVERT;
- break;
- case 'H':
- *videoflip = HFLIP;
- break;
- case 'V':
- *videoflip = VFLIP;
- break;
- default:
- return false;
+ case 'I':
+ *videoflip = INVERT;
+ break;
+ case 'H':
+ *videoflip = HFLIP;
+ break;
+ case 'V':
+ *videoflip = VFLIP;
+ break;
+ default:
+ return false;
}
return true;
}
@@ -866,14 +866,14 @@ static bool get_videoflip (const char *str, videoflip_t *videoflip) {
static bool get_videorotate (const char *str, videoflip_t *videoflip) {
if (strlen(str) > 1) return false;
switch (str[0]) {
- case 'L':
- *videoflip = LEFT;
- break;
- case 'R':
- *videoflip = RIGHT;
- break;
- default:
- return false;
+ case 'L':
+ *videoflip = LEFT;
+ break;
+ case 'R':
+ *videoflip = RIGHT;
+ break;
+ default:
+ return false;
}
return true;
}
@@ -903,17 +903,17 @@ static void parse_arguments (int argc, char *argv[]) {
// Parse arguments
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
- if (arg == "-rc") {
+ if (arg == "-rc") {
i++; //specifies startup file: has already been processed
} else if (arg == "-allow") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
i++;
- allowed_clients.push_back(argv[i]);
- } else if (arg == "-block") {
+ allowed_clients.push_back(argv[i]);
+ } else if (arg == "-block") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
i++;
- blocked_clients.push_back(argv[i]);
- } else if (arg == "-restrict") {
+ blocked_clients.push_back(argv[i]);
+ } else if (arg == "-restrict") {
if (i < argc - 1) {
if (strlen(argv[i+1]) == 2 && strncmp(argv[i+1], "no", 2) == 0) {
restrict_clients = false;
@@ -942,7 +942,7 @@ static void parse_arguments (int argc, char *argv[]) {
if (n > -SECOND_IN_USECS && n < SECOND_IN_USECS) {
audio_delay_alac = n * 1000; /* units are nsecs */
} else {
- fprintf(stderr, "invalid -async %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i] );
+ fprintf(stderr, "invalid -async %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i] );
exit (1);
}
}
@@ -962,7 +962,7 @@ static void parse_arguments (int argc, char *argv[]) {
if (n > -SECOND_IN_USECS && n < SECOND_IN_USECS) {
audio_delay_aac = n * 1000; /* units are nsecs */
} else {
- fprintf(stderr, "invalid -vsync %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i]);
+ fprintf(stderr, "invalid -vsync %s: requested delays must be smaller than +/- 1000 millisecs\n", argv[i]);
exit (1);
}
}
@@ -1017,7 +1017,7 @@ static void parse_arguments (int argc, char *argv[]) {
}
}
} else if (arg == "-m") {
- if (i < argc - 1 && *argv[i+1] != '-') {
+ if (i < argc - 1 && *argv[i+1] != '-') {
if (validate_mac(argv[++i])) {
mac_address.erase();
mac_address = argv[i];
@@ -1033,18 +1033,18 @@ static void parse_arguments (int argc, char *argv[]) {
} else if (arg == "-a") {
use_audio = false;
} else if (arg == "-d") {
- if (i < argc - 1 && *argv[i+1] != '-') {
+ if (i < argc - 1 && *argv[i+1] != '-') {
unsigned int n = 1;
if (!get_value(argv[++i], &n)) {
fprintf(stderr, "invalid \"-d %s\"; -d n : max n=1 (suppress packet data in debug output)\n", argv[i]);
exit(1);
}
debug_log = true;
- suppress_packet_debug_data = true;
- } else {
+ suppress_packet_debug_data = true;
+ } else {
debug_log = !debug_log;
- suppress_packet_debug_data = false;
- }
+ suppress_packet_debug_data = false;
+ }
} else if (arg == "-h" || arg == "--help" || arg == "-?" || arg == "-help") {
print_info(argv[0]);
exit(0);
@@ -1068,10 +1068,10 @@ static void parse_arguments (int argc, char *argv[]) {
videosink.erase();
videosink.append(argv[++i]);
std::size_t pos = videosink.find(" ");
- if (pos != std::string::npos) {
+ if (pos != std::string::npos) {
videosink_options.erase();
videosink_options = videosink.substr(pos);
- videosink.erase(pos);
+ videosink.erase(pos);
}
} else if (arg == "-as") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
@@ -1083,7 +1083,7 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
} else if (arg == "-nc") {
new_window_closing_behavior = false;
- if (i < argc - 1) {
+ if (i < argc - 1) {
if (strlen(argv[i+1]) == 2 && strncmp(argv[i+1], "no", 2) == 0) {
new_window_closing_behavior = true;
i++;
@@ -1113,7 +1113,7 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
} else if (arg == "-fs" ) {
fullscreen = true;
- } else if (arg == "-FPSdata") {
+ } else if (arg == "-FPSdata") {
show_client_FPS_data = true;
} else if (arg == "-reset") {
/* now using feedback (every 1 sec ) instead of ntp timeouts (every 3 secs) to detect offline client and reset connections */
@@ -1214,7 +1214,7 @@ static void parse_arguments (int argc, char *argv[]) {
int n;
char *end;
if (i < argc - 1 && *argv[i+1] != '-') {
- n = (int) (strtof(argv[++i], &end) * SECOND_IN_USECS);
+ n = (int) (strtof(argv[++i], &end) * SECOND_IN_USECS);
if (*end == '\0' && n >=0 && n <= 10 * SECOND_IN_USECS) {
audiodelay = n;
continue;
@@ -1226,7 +1226,7 @@ static void parse_arguments (int argc, char *argv[]) {
} else if (arg == "-pin") {
setup_legacy_pairing = true;
pin_pw = 1;
- if (i < argc - 1 && *argv[i+1] != '-') {
+ if (i < argc - 1 && *argv[i+1] != '-') {
unsigned int n = 9999;
if (!get_value(argv[++i], &n)) {
fprintf(stderr, "invalid \"-pin %s\"; -pin nnnn : max nnnn=9999, (4 digits)\n", argv[i]);
@@ -1255,10 +1255,10 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
}
} else {
- // fprintf(stderr, "option \"-key \" requires a path to a file for persistent key storage\n");
- // exit(1);
- keyfile.erase();
- keyfile.append("0");
+ // fprintf(stderr, "option \"-key \" requires a path to a file for persistent key storage\n");
+ // exit(1);
+ keyfile.erase();
+ keyfile.append("0");
}
} else if (arg == "-pw") {
setup_legacy_pairing = false;
@@ -1282,15 +1282,15 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "%s cannot be written to:\noption \"-dacp \" must be to a file with write access\n", fn);
exit(1);
}
- } else {
+ } else {
dacpfile.append(get_homedir());
dacpfile.append("/.uxplay.dacp");
}
- } else if (arg == "-taper") {
+ } else if (arg == "-taper") {
taper_volume = true;
} else if (arg == "-db") {
bool db_bad = true;
- double db1, db2;
+ double db1, db2;
if ( i < argc -1) {
char *end1, *end2;
db1 = strtod(argv[i+1], &end1);
@@ -1308,7 +1308,7 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "invalid \"-db %s\": db value must be \"low\" or \"low:high\", low < 0 and high > low are decibel gains\n", argv[i+1]);
exit(1);
}
- i++;
+ i++;
db_low = db1;
db_high = db2;
printf("db range %f:%f\n", db_low, db_high);
@@ -1337,7 +1337,7 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "invalid \"-vol %s\", value must be between 0.0 (mute) and 1.0 (full volume)\n", argv[i+1]);
exit(1);
}
- i++;
+ i++;
} else if (arg == "-hls") {
hls_support = true;
if (i < argc - 1 && *argv[i+1] != '-') {
@@ -1346,7 +1346,7 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr, "invalid \"-hls %s\"; -hls n only allows \"playbin\" video player versions 2 or 3\n", argv[i]);
exit(1);
}
- playbin_version = (guint) n;
+ playbin_version = (guint) n;
}
} else if (arg == "-h265") {
h265_support = true;
@@ -1463,7 +1463,7 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
char *str = (char *) calloc(datalen + 1, sizeof(char));
memcpy(str, metadata, datalen);
metadata_text->append(str);
- metadata_text->append("\n");
+ metadata_text->append("\n");
free(str);
} else if (debug_log) {
std::string md = "";
@@ -1485,7 +1485,7 @@ static int parse_dmap_header(const unsigned char *metadata, char *tag, int *len)
bool istag = true;
for (int i = 0; i < 4; i++) {
tag[i] = (char) *header;
- if (!isalpha(tag[i])) {
+ if (!isalpha(tag[i])) {
istag = false;
}
header++;
@@ -1801,10 +1801,10 @@ extern "C" void report_client_request(void *cls, char *deviceid, char * model, c
LOGI("connection request from %s (%s) with deviceID = %s\n", name, model, deviceid);
if (restrict_clients) {
*admit = check_client(deviceid);
- if (*admit == false) {
+ if (*admit == false) {
LOGI("client connections have been restricted to those with listed deviceID,\nuse \"-allow %s\" to allow this client to connect.\n",
deviceid);
- }
+ }
} else {
*admit = true;
}
@@ -1853,8 +1853,8 @@ extern "C" void video_process (void *cls, raop_ntp_t *ntp, video_decode_struct *
remote_clock_offset = local_time - data->ntp_time_remote;
}
int count = 0;
- uint64_t pts_mismatch = 0;
- do {
+ uint64_t pts_mismatch = 0;
+ do {
data->ntp_time_remote = data->ntp_time_remote + remote_clock_offset;
pts_mismatch = video_renderer_render_buffer(data->data, &(data->data_len), &(data->nal_count), &(data->ntp_time_remote));
if (pts_mismatch) {
@@ -1926,15 +1926,15 @@ extern "C" void audio_set_volume (void *cls, float volume) {
/* flat rescaling of decibel range from {-30dB : 0dB} to {db_low : db_high} */
db_flat = db_low + (db_high-db_low) * frac;
if (taper_volume) {
- /* taper the volume reduction by the (rescaled) Airplay {-30:0} range so each reduction of
- * the remaining slider length by 50% reduces the perceived volume by 50% (-10dB gain)
- * (This is the "dasl-tapering" scheme offered by shairport-sync) */
+ /* taper the volume reduction by the (rescaled) Airplay {-30:0} range so each reduction of
+ * the remaining slider length by 50% reduces the perceived volume by 50% (-10dB gain)
+ * (This is the "dasl-tapering" scheme offered by shairport-sync) */
db = db_high + 10.0 * (log10(frac) / log10(2.0));
db = (db > db_flat) ? db : db_flat;
- } else {
+ } else {
db = db_flat;
}
- /* conversion from (gain) decibels to GStreamer's linear volume scale */
+ /* conversion from (gain) decibels to GStreamer's linear volume scale */
gst_volume = pow(10.0, 0.05*db);
}
audio_renderer_set_volume(gst_volume);
@@ -2132,24 +2132,20 @@ extern "C" void on_video_acquire_playback_info (void *cls, playback_info_t *play
extern "C" void log_callback (void *cls, int level, const char *msg) {
switch (level) {
- case LOGGER_DEBUG: {
- LOGD("%s", msg);
- break;
- }
- case LOGGER_WARNING: {
- LOGW("%s", msg);
- break;
- }
- case LOGGER_INFO: {
- LOGI("%s", msg);
- break;
- }
- case LOGGER_ERR: {
- LOGE("%s", msg);
- break;
- }
- default:
- break;
+ case LOGGER_DEBUG:
+ LOGD("%s", msg);
+ break;
+ case LOGGER_WARNING:
+ LOGW("%s", msg);
+ break;
+ case LOGGER_INFO:
+ LOGI("%s", msg);
+ break;
+ case LOGGER_ERR:
+ LOGE("%s", msg);
+ break;
+ default:
+ break;
}
}
@@ -2306,7 +2302,7 @@ static void read_config_file(const char * filename, const char * uxplay_name) {
} else {
options.push_back(token.c_str());
}
- }
+ }
}
}
file.close();
@@ -2365,7 +2361,7 @@ int main (int argc, char *argv[]) {
exit(1);
}
rcfile = argv[i+1];
- if (stat(rcfile, &sb) == -1) {
+ if (stat(rcfile, &sb) == -1) {
LOGE("startup file %s specified by option -rc was not found", rcfile);
exit(0);
}
@@ -2404,7 +2400,7 @@ int main (int argc, char *argv[]) {
if (dump_video) {
if (video_dump_limit > 0) {
LOGI("dump video using \"-vdmp %d %s\"", video_dump_limit, video_dumpfile_name.c_str());
- } else {
+ } else {
LOGI("dump video using \"-vdmp %s\"", video_dumpfile_name.c_str());
}
}
@@ -2449,9 +2445,9 @@ int main (int argc, char *argv[]) {
if (fullscreen && use_video) {
if (videosink == "waylandsink" || videosink == "vaapisink") {
videosink_options.append(" fullscreen=true");
- } else if (videosink == "kmssink") {
+ } else if (videosink == "kmssink") {
videosink_options.append(" force_modesetting=true");
- }
+ }
}
if (videosink == "d3d11videosink" && videosink_options.empty() && use_video) {
@@ -2487,8 +2483,8 @@ int main (int argc, char *argv[]) {
if (pairing_register == "") {
const char * homedir = get_homedir();
if (homedir) {
- pairing_register = homedir;
- pairing_register.append("/.uxplay.register");
+ pairing_register = homedir;
+ pairing_register.append("/.uxplay.register");
}
}
}
@@ -2498,13 +2494,13 @@ int main (int argc, char *argv[]) {
size_t len = 0;
std::string key;
int clients = 0;
- std::ifstream file(pairing_register);
+ std::ifstream file(pairing_register);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
/*32 bytes pk -> base64 -> strlen(pk64) = 44 chars = line[0:43]; add '\0' at line[44] */
line[44] = '\0';
- std::string pk = line.c_str();
+ std::string pk = line.c_str();
registered_keys.push_back(key.assign(pk));
clients ++;
}
@@ -2544,7 +2540,7 @@ int main (int argc, char *argv[]) {
logger_set_level(render_logger, log_level);
if (use_audio) {
- audio_renderer_init(render_logger, audiosink.c_str(), &audio_sync, &video_sync);
+ audio_renderer_init(render_logger, audiosink.c_str(), &audio_sync, &video_sync);
} else {
LOGI("audio_disabled");
}
From e7d2901b7f07f0da11a3d97ce1ae54b659bd4431 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 8 Aug 2025 19:42:01 -0400
Subject: [PATCH 11/58] add video packet length to debug output
(raop_rtp_mirror)
---
lib/raop_rtp_mirror.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c
index 029c6d5..d4d7908 100644
--- a/lib/raop_rtp_mirror.c
+++ b/lib/raop_rtp_mirror.c
@@ -409,9 +409,9 @@ raop_rtp_mirror_thread(void *arg)
uint64_t ntp_now = raop_ntp_get_local_time();
int64_t latency = (ntp_timestamp_local ? ((int64_t) ntp_now) - ((int64_t) ntp_timestamp_local) : 0);
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG,
- "raop_rtp video: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, %s %s",
+ "raop_rtp video: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, %s %s, size: %d",
(double) ntp_now / SEC, (double) ntp_timestamp_local / SEC, (double) latency / SEC,
- (double) ntp_timestamp_remote / SEC, packet_description, h265_video ? h265 : h264);
+ (double) ntp_timestamp_remote / SEC, packet_description, (h265_video ? h265 : h264), payload_size);
}
unsigned char* payload_out;
@@ -452,7 +452,7 @@ raop_rtp_mirror_thread(void *arg)
payload_out = (unsigned char*) malloc(payload_size);
payload_decrypted = payload_out;
}
- // Decrypt data
+ // Decrypt data: AES-CTR encryption/decryption does not change the size of the data
mirror_buffer_decrypt(raop_rtp_mirror->buffer, payload, payload_decrypted, payload_size);
// It seems the AirPlay protocol prepends NALs with their size, which we're replacing with the 4-byte
From ab9a9d20b800f694e3883012538cf349ff202537 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 9 Aug 2025 07:37:17 -0400
Subject: [PATCH 12/58] whitespace
---
lib/raop_rtp.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/raop_rtp.c b/lib/raop_rtp.c
index 51c304e..85f7de7 100644
--- a/lib/raop_rtp.c
+++ b/lib/raop_rtp.c
@@ -473,7 +473,7 @@ raop_rtp_thread_udp(void *arg)
raop_rtp->control_saddr_len = saddrlen;
got_remote_control_saddr = true;
}
- } else {
+ } else {
packetlen = recvfrom(raop_rtp->csock, (char *)packet, sizeof(packet), 0, NULL, NULL);
}
int type_c = packet[1] & ~0x80;
From 59c3abe140535e3bbac4f01189740c8c944b3594 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Mon, 11 Aug 2025 11:36:03 -0400
Subject: [PATCH 13/58] add mirror video activity monitoring (forscreensaver
inhibition)
---
lib/raop.h | 3 ++-
lib/raop_rtp_mirror.c | 16 ++++++++++++----
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/lib/raop.h b/lib/raop.h
index 6cbe3d6..87f486b 100644
--- a/lib/raop.h
+++ b/lib/raop.h
@@ -12,7 +12,7 @@
* Lesser General Public License for more details.
*
*===================================================================
- * modified by fduncanh 2021-23
+ * modified by fduncanh 2021-25
*/
#ifndef RAOP_H
@@ -87,6 +87,7 @@ struct raop_callbacks_s {
void (*audio_set_progress)(void *cls, unsigned int start, unsigned int curr, unsigned int end);
void (*audio_get_format)(void *cls, unsigned char *ct, unsigned short *spf, bool *usingScreen, bool *isMedia, uint64_t *audioFormat);
void (*video_report_size)(void *cls, float *width_source, float *height_source, float *width, float *height);
+ void (*mirror_video_activity)(void *cls, double *txusage);
void (*report_client_request) (void *cls, char *deviceid, char *model, char *name, bool *admit);
void (*display_pin) (void *cls, char * pin);
void (*register_client) (void *cls, const char *device_id, const char *pk_str, const char *name);
diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c
index d4d7908..e2a00f0 100644
--- a/lib/raop_rtp_mirror.c
+++ b/lib/raop_rtp_mirror.c
@@ -795,7 +795,7 @@ raop_rtp_mirror_thread(void *arg)
* Sometimes (e.g, when the client has a locked screen), there is a 25kB trailer attached to the packet. *
* This 25000 Byte trailer with unidentified content seems to be the same data each time it is sent. */
- if (payload_size && raop_rtp_mirror->show_client_FPS_data) {
+ if (payload_size) {
//char *str = utils_data_to_string(packet, 128, 16);
//logger_log(raop_rtp_mirror->logger, LOGGER_WARNING, "type 5 video packet header:\n%s", str);
//free (str);
@@ -815,9 +815,17 @@ raop_rtp_mirror_thread(void *arg)
uint32_t plist_len;
plist_t root_node = NULL;
plist_from_bin((char *) payload, plist_size, &root_node);
- plist_to_xml(root_node, &plist_xml, &plist_len);
- logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "%s", plist_xml);
- free(plist_xml);
+ if (raop_rtp_mirror->callbacks.mirror_video_activity) {
+ double txusage = 0.0;
+ plist_t tx_usage_avg_node = plist_dict_get_item(root_node, "txUsageAvg");
+ plist_get_real_val(tx_usage_avg_node, &txusage);
+ raop_rtp_mirror->callbacks.mirror_video_activity(raop_rtp_mirror->callbacks.cls, &txusage);
+ }
+ if (raop_rtp_mirror->show_client_FPS_data) {
+ plist_to_xml(root_node, &plist_xml, &plist_len);
+ logger_log(raop_rtp_mirror->logger, LOGGER_INFO, "%s", plist_xml);
+ free(plist_xml);
+ }
}
}
break;
From 9d28d2770216551abb30e785b5ffb2e8bfbfbe89 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Mon, 11 Aug 2025 14:23:06 -0400
Subject: [PATCH 14/58] added support for DBus-based screensaver inhibition
---
CMakeLists.txt | 14 ++++
README.html | 28 ++++++-
README.md | 26 ++++++-
README.txt | 29 +++++++-
uxplay.1 | 2 +
uxplay.cpp | 193 +++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 286 insertions(+), 6 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 61f6d5e..d797d79 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -31,6 +31,15 @@ if ( ( UNIX AND NOT APPLE ) OR USE_X11 )
endif()
endif()
+if( UNIX AND NOT APPLE )
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(DBUS dbus-1>=1.4.12)
+ if (DBUS_FOUND )
+ add_definitions(-DDBUS )
+ include_directories(${DBUS_INCLUDE_DIRS})
+ endif()
+endif()
+
if( UNIX AND NOT APPLE )
add_definitions( -DSUPPRESS_AVAHI_COMPAT_WARNING )
# convert AirPlay colormap 1:3:7:1 to sRGB (1:1:7:1), needed on Linux and BSD
@@ -54,6 +63,11 @@ target_link_libraries( uxplay
renderers
airplay
)
+if (DBUS_FOUND)
+target_link_libraries( uxplay
+ ${DBUS_LIBRARIES}
+ )
+endif()
install( TARGETS uxplay RUNTIME DESTINATION bin )
install( FILES uxplay.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
diff --git a/README.html b/README.html
index 6aeef12..b98ce60 100644
--- a/README.html
+++ b/README.html
@@ -8,6 +8,15 @@ developed at the GitHub site https://github.com/FDH2/UxPlay (where ALL user issues
should be posted, and latest versions can be found).
+NEW on github: (for Linux/*BSD Desktop
+Environments using D-Bus). New option -scrsv <n>
+provides screensaver inhibition (e.g., to prevent screensaver function
+while watching mirrored videos without keyboard or mouse activity): n =
+0 (off) n=1 (on during video activity) n=2 (always on while UxPlay is
+running). Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment
+for other Desktop Environments (please report). (watch output of
+dbus-monitor to verify that inhibition is working).
+Might not work on Wayland.
NEW on github: option -ca (with no filename
given) will now render Apple Music cover art (in audio-only mode) inside
UxPlay. (-ca <filename> will continue to export cover
@@ -125,6 +134,10 @@ you may wish to add “as pipewiresink” or “vs waylandsink” as defaults to
the file. (Output from terminal commands “ps waux | grep pulse” or
“pactl info” will contain “pipewire” if your Linux/BSD system uses
it).
+For Linux/*BSD systems using D-Bus, the option
+-scrsv 1 inhibits the screensaver while there is video
+activity on UxPlay (-scrsv 2 inhibits it whenever UxPlay is
+running).
For Linux systems using systemd, there is a
systemd service file uxplay.service
found in the UxPlay top directory of the distribution, and also
@@ -1042,6 +1055,16 @@ default: 3) allows selection of the version of GStreamer’s "playbin"
video player to use for playing HLS video. (Playbin v3 is the
recommended player, but if some videos fail to play, you can try with
version 2.)
+-scrsv n. (since 1.73) (So far, only implemented on
+Linux/*BSD systems using D-Bus). Inhibit the screensaver in the absence
+of keyboard input (e.g., while watching video), using the
+org.freedesktop.ScreenSaver D-Bus service: n = 0: (off) n= 1 (on during
+video activity) n=2 (always on). Note: to verify this feature is
+working, you can use dbus-monitor to view events on the
+D-Bus; depending on the Desktop Environment, commands like
+gnome-session-inhibit -l,
+xfce4-screensaver-commannd -q, etc., should list UxPlay
+when it is inhibiting the screensaver.
-pin [nnnn]: (since v1.67) use Apple-style
(one-time) “pin” authentication when a new client connects for the first
time: a four-digit pin code is displayed on the terminal, and the client
@@ -1717,8 +1740,9 @@ an AppleTV6,2 with sourceVersion 380.20.1 (an AppleTV 4K 1st gen,
introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
Changelog
-xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option
-(no file specified).
+xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option
+(no file specified). (D-Bus based) option -scrsv to inhibit
+screensaver while UxPlay is running (Linux/*BSD only).
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced
with -pw option. Update llhttp to v 9.3.0
1.72.1 2025-06-06 minor update: fix regression in -reg option; add
diff --git a/README.md b/README.md
index 2817808..03f15b8 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,12 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
+- **NEW on github**: (for Linux/*BSD Desktop Environments using D-Bus). New option `-scrsv ` provides screensaver inhibition (e.g., to
+ prevent screensaver function while watching mirrored videos without keyboard or mouse
+ activity): n = 0 (off) n=1 (on during video activity) n=2 (always on while UxPlay is running).
+ Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment for other Desktop Environments (please report).
+ (watch output of `dbus-monitor` to verify that inhibition is working). _Might not work on Wayland_.
+
- **NEW on github**: option -ca (with no filename given) will now render
Apple Music cover art (in audio-only mode) inside
UxPlay. (-ca `` will continue to export cover art for
@@ -114,6 +120,9 @@ status](https://repology.org/badge/vertical-allrepos/uxplay.svg)](https://repolo
from terminal commands "ps waux \| grep pulse" or "pactl info" will
contain "pipewire" if your Linux/BSD system uses it).*
+- For Linux/*BSD systems using D-Bus, the option `-scrsv 1` inhibits the screensaver while
+ there is video activity on UxPlay (`-scrsv 2` inhibits it whenever UxPlay is running).
+
- For Linux systems using systemd, there is a **systemd** service file **uxplay.service**
found in the UxPlay top directory of the distribution, and also installed
in `/uxplay/systemd/` (where DOCDIR is usually ``/usr/local/share/doc``), that allows users to start
@@ -1036,6 +1045,18 @@ allows selection of the version of GStreamer's
is the recommended player, but if some videos fail to play, you can try
with version 2.)_
+**-scrsv n**. (since 1.73) (So far, only implemented
+on Linux/*BSD systems using D-Bus). Inhibit the screensaver in the
+absence of keyboard input (e.g., while watching video), using the
+org.freedesktop.ScreenSaver D-Bus service:
+n = 0: (off) n= 1 (on during video activity) n=2 (always on).
+_Note: to verify this feature is working, you can use `dbus-monitor`
+to view events on the D-Bus; depending on the Desktop Environment,
+commands like
+`gnome-session-inhibit -l`, ``xfce4-screensaver-commannd -q``, etc.,
+should list UxPlay when it is inhibiting
+the screensaver._
+
**-pin \[nnnn\]**: (since v1.67) use Apple-style (one-time) "pin"
authentication when a new client connects for the first time: a
four-digit pin code is displayed on the terminal, and the client screen
@@ -1752,8 +1773,9 @@ introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
# Changelog
-xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option (no file
-specified).
+xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option (no file
+specified). (D-Bus based) option -scrsv to inhibit screensaver while UxPlay
+is running (Linux/*BSD only).
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with -pw
option. Update llhttp to v 9.3.0
diff --git a/README.txt b/README.txt
index 8e84d2d..65e295f 100644
--- a/README.txt
+++ b/README.txt
@@ -2,6 +2,16 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
+- **NEW on github**: (for Linux/\*BSD Desktop Environments using
+ D-Bus). New option `-scrsv ` provides screensaver inhibition
+ (e.g., to prevent screensaver function while watching mirrored
+ videos without keyboard or mouse activity): n = 0 (off) n=1 (on
+ during video activity) n=2 (always on while UxPlay is running).
+ Tested on Gnome/KDE/Cinnamon/Mate/Xfce 4: may need adjustment for
+ other Desktop Environments (please report). (watch output of
+ `dbus-monitor` to verify that inhibition is working). *Might not
+ work on Wayland*.
+
- **NEW on github**: option -ca (with no filename given) will now
render Apple Music cover art (in audio-only mode) inside UxPlay.
(-ca `` will continue to export cover art for display by
@@ -121,6 +131,10 @@ status](https://repology.org/badge/vertical-allrepos/uxplay.svg)](https://repolo
commands "ps waux \| grep pulse" or "pactl info" will contain
"pipewire" if your Linux/BSD system uses it).*
+- For Linux/\*BSD systems using D-Bus, the option `-scrsv 1` inhibits
+ the screensaver while there is video activity on UxPlay (`-scrsv 2`
+ inhibits it whenever UxPlay is running).
+
- For Linux systems using systemd, there is a **systemd** service file
**uxplay.service** found in the UxPlay top directory of the
distribution, and also installed in `/uxplay/systemd/`
@@ -1055,6 +1069,16 @@ allows selection of the version of GStreamer's \"playbin\" video player
to use for playing HLS video. *(Playbin v3 is the recommended player,
but if some videos fail to play, you can try with version 2.)*
+**-scrsv n**. (since 1.73) (So far, only implemented on Linux/\*BSD
+systems using D-Bus). Inhibit the screensaver in the absence of keyboard
+input (e.g., while watching video), using the
+org.freedesktop.ScreenSaver D-Bus service: n = 0: (off) n= 1 (on during
+video activity) n=2 (always on). *Note: to verify this feature is
+working, you can use `dbus-monitor` to view events on the D-Bus;
+depending on the Desktop Environment, commands like
+`gnome-session-inhibit -l`, `xfce4-screensaver-commannd -q`, etc.,
+should list UxPlay when it is inhibiting the screensaver.*
+
**-pin \[nnnn\]**: (since v1.67) use Apple-style (one-time) "pin"
authentication when a new client connects for the first time: a
four-digit pin code is displayed on the terminal, and the client screen
@@ -1768,8 +1792,9 @@ what version UxPlay claims to be.
# Changelog
-xxxx 2025-07-07 Render Audio cover-art inside UxPlay with -ca option (no
-file specified).
+xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option (no
+file specified). (D-Bus based) option -scrsv ``{=html} to inhibit
+screensaver while UxPlay is running (Linux/\*BSD only).
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with
-pw option. Update llhttp to v 9.3.0
diff --git a/uxplay.1 b/uxplay.1
index 3cda48d..0c429bb 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -19,6 +19,8 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.IP
v = 2 or 3 (default 3) optionally selects video player version
.TP
+\fB\-scrsv\fI n\fR Screensaver override \fIn\fR:0=off 1=on during activity 2=always on.
+.TP
\fB\-pin\fI[xxxx]\fRUse a 4-digit pin code to control client access (default: no)
.IP
without option, pin is random: optionally use fixed pin xxxx.
diff --git a/uxplay.cpp b/uxplay.cpp
index 4a9abe4..dc1e1ea 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -35,6 +35,7 @@
#include
#include
#include
+#include
#ifdef _WIN32 /*modifications for Windows compilation */
#include
@@ -64,6 +65,10 @@
#include "lib/dnssd.h"
#include "renderers/video_renderer.h"
#include "renderers/audio_renderer.h"
+#ifdef DBUS
+#include
+#endif
+
#define VERSION "1.72"
@@ -175,6 +180,28 @@ static guint missed_feedback_limit = MISSED_FEEDBACK_LIMIT;
static guint missed_feedback = 0;
static guint playbin_version = DEFAULT_PLAYBIN_VERSION;
static bool reset_httpd = false;
+
+//Support for D-Bus-based screensaver inhibition (org.freedesktop.ScreenSaver)
+static unsigned int scrsv;
+#ifdef DBUS
+/* these strings can be changed at startup if a non-conforming Desktop Environmemt is detected */
+static std::string dbus_service = "org.freedesktop.ScreenSaver";
+static std::string dbus_path = "/org/freedesktop/ScreenSaver";
+static std::string dbus_interface = "org.freedesktop.ScreenSaver";
+static std::string dbus_inhibit = "Inhibit";
+static std::string dbus_uninhibit = "UnInhibit";
+static DBusConnection *dbus_connection = NULL;
+static dbus_uint32_t dbus_cookie = 0;
+static DBusPendingCall *dbus_pending = NULL;
+static bool dbus_last_message = false;
+static const char *appname = DEFAULT_NAME;
+static const char *reason_always = "mirroring client: inhibit always";
+static const char *reason_active = "actively receiving video";
+static int activity_count;
+static double activity_threshold = 500000.0; // threshold for FPSdata item txUsageAvg to classify mirror video as "active"
+#define MAX_ACTIVITY_COUNT 60
+#endif
+
/* logging */
static void log(int level, const char* format, ...) {
@@ -204,6 +231,73 @@ static void log(int level, const char* format, ...) {
#define LOGW(...) log(LOGGER_WARNING, __VA_ARGS__)
#define LOGE(...) log(LOGGER_ERR, __VA_ARGS__)
+#ifdef DBUS
+static void dbus_screensaver_inhibiter(bool inhibit) {
+ g_assert(inhibit != dbus_last_message);
+ g_assert(scrsv);
+ /* receive reply from previous request, whenever that was sent
+ * (may have been sent hours ago ... !)
+ * (code modeled on vlc/modules/misc/inhibit/dbus.c) */
+ if (dbus_pending != NULL) {
+ DBusMessage *reply;
+ dbus_pending_call_block(dbus_pending);
+ reply = dbus_pending_call_steal_reply(dbus_pending);
+ dbus_pending_call_unref(dbus_pending);
+ dbus_pending = NULL;
+ if (reply != NULL) {
+ if (!dbus_message_get_args(reply, NULL,
+ DBUS_TYPE_UINT32, &dbus_cookie,
+ DBUS_TYPE_INVALID)) {
+ dbus_cookie = 0;
+ }
+ dbus_message_unref(reply);
+ }
+ LOGD("screen_saver: got D-Bus cookie %" PRIu32, (uint32_t) dbus_cookie);
+ }
+
+ if (!dbus_cookie && !inhibit) {
+ return; /* nothing to do */
+ }
+
+ /* send request */
+ const char *dbus_method = inhibit ? dbus_inhibit.c_str() : dbus_uninhibit.c_str();
+ DBusMessage *dbus_message = dbus_message_new_method_call(dbus_service.c_str(),
+ dbus_path.c_str(),
+ dbus_interface.c_str(),
+ dbus_method);
+ g_assert (dbus_message);
+
+ if (inhibit) {
+ dbus_bool_t ret;
+ const char *reason = (scrsv == 1) ? reason_active : reason_always;
+
+ ret = dbus_message_append_args(dbus_message,
+ DBUS_TYPE_STRING, &appname,
+ DBUS_TYPE_STRING, &reason,
+ DBUS_TYPE_INVALID);
+ g_assert(ret);
+
+ ret = dbus_connection_send_with_reply(dbus_connection, dbus_message, &dbus_pending, -1);
+ if (!ret) {
+ dbus_pending = NULL;
+ }
+ } else {
+ g_assert(dbus_cookie);
+ LOGD("screen_saver: releasing D-Bus cookie %" PRIu32, (uint32_t) dbus_cookie);
+ if (dbus_message_append_args(dbus_message,
+ DBUS_TYPE_UINT32, &dbus_cookie,
+ DBUS_TYPE_INVALID)
+ && dbus_connection_send(dbus_connection, dbus_message, NULL)) {
+ dbus_cookie = 0;
+ }
+ }
+
+ dbus_connection_flush(dbus_connection);
+ dbus_message_unref(dbus_message);
+ dbus_last_message = inhibit;
+}
+#endif
+
static bool file_has_write_access (const char * filename) {
bool exists = false;
bool write = false;
@@ -683,6 +777,7 @@ static void print_info (char *name) {
printf("-h265 Support h265 (4K) video (with h265 versions of h264 plugins)\n");
printf("-hls [v] Support HTTP Live Streaming (currently Youtube video only) \n");
printf(" v = 2 or 3 (default 3) optionally selects video player version\n");
+ printf("-scrsv n Screensaver override n: 0=off 1=on during activity 2=always on\n");
printf("-pin[xxxx]Use a 4-digit pin code to control client access (default: no)\n");
printf(" default pin is random: optionally use fixed pin xxxx\n");
printf("-reg [fn] Keep a register in $HOME/.uxplay.register to verify returning\n");
@@ -947,6 +1042,19 @@ static void parse_arguments (int argc, char *argv[]) {
}
}
}
+ } else if (arg == "-scrsv") {
+ if (!option_has_value(i, argc, argv[i], argv[i+1])) exit(1);
+ unsigned int n = 0;
+ if (!get_value(argv[++i], &n) || n > 2) {
+ fprintf(stderr, "invalid \"-scrsv %s\"; values 0, 1, 2 allowed\n", argv[i]);
+ exit(1);
+ }
+#ifdef DBUS
+ scrsv = n;
+#else
+ fprintf(stderr,"invalid: option \"-scrsv\" is currently only implemented for Linux/*BSD systems with D-Bus service\n");
+ exit(1);
+#endif
} else if (arg == "-vsync") {
video_sync = true;
if (i < argc - 1) {
@@ -1866,6 +1974,27 @@ extern "C" void video_process (void *cls, raop_ntp_t *ntp, video_decode_struct *
}
}
+#ifdef DBUS
+extern "C" void mirror_video_activity (void *cls, double *txusage) {
+ if (scrsv != 1) {
+ return;
+ }
+ if (*txusage > activity_threshold) {
+ if (activity_count < MAX_ACTIVITY_COUNT) {
+ activity_count++;
+ } else if (activity_count == MAX_ACTIVITY_COUNT && !dbus_last_message) {
+ dbus_screensaver_inhibiter(true);
+ }
+ } else {
+ if (activity_count > 0) {
+ activity_count--;
+ } else if (activity_count == 0 && dbus_last_message) {
+ dbus_screensaver_inhibiter(false);
+ }
+ }
+}
+#endif
+
extern "C" void video_pause (void *cls) {
if (use_video) {
video_renderer_pause();
@@ -2179,6 +2308,9 @@ static int start_raop_server (unsigned short display[5], unsigned short tcp[3],
raop_cbs.export_dacp = export_dacp;
raop_cbs.video_reset = video_reset;
raop_cbs.video_set_codec = video_set_codec;
+#ifdef DBUS
+ raop_cbs.mirror_video_activity = mirror_video_activity;
+#endif
raop_cbs.on_video_play = on_video_play;
raop_cbs.on_video_scrub = on_video_scrub;
raop_cbs.on_video_rate = on_video_rate;
@@ -2310,6 +2442,7 @@ static void read_config_file(const char * filename, const char * uxplay_name) {
fprintf(stderr,"UxPlay: failed to open configuration file at %s\n", config_file.c_str());
}
if (options.size() > 1) {
+
int argc = options.size();
char **argv = (char **) malloc(sizeof(char*) * argc);
for (int i = 0; i < argc; i++) {
@@ -2319,6 +2452,7 @@ static void read_config_file(const char * filename, const char * uxplay_name) {
free (argv);
}
}
+
#ifdef GST_MACOS
/* workaround for GStreamer >= 1.22 "Official Builds" on macOS */
#include
@@ -2393,6 +2527,52 @@ int main (int argc, char *argv[]) {
LOGI("UxPlay %s: An Open-Source AirPlay mirroring and audio-streaming server.", VERSION);
+#ifdef DBUS
+ if (scrsv) {
+ DBusError dbus_error;
+ dbus_error_init(&dbus_error);
+ dbus_connection = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error);
+ if (dbus_error_is_set(&dbus_error)) {
+ dbus_error_free(&dbus_error);
+ scrsv = 0;
+ LOGI ("D-Bus session not found: screensaver inhibition option (\"-scrsv\") will not be active");
+ }
+ }
+ if (scrsv) {
+ LOGD ("D-Bus session support is available, connection %p", dbus_connection);
+ std::string desktop = getenv("XDG_CURRENT_DESKTOP");
+ LOGD("Desktop Environment: %s", desktop.c_str());
+
+ /* if dbus_service, dbus_path, dbus_interface, dbus_inhibit, dbus_uninhibit *
+ * in the detected Desktop Environments are still non-conforming to the *
+ * org.freedesktop.ScreenSaver interface, they can be modifed here */
+
+ /* some desktop environments (e.g. Xfce 4, Mate) modify the D-Bus service name */
+ std::string name;
+ if (strstr(desktop.c_str(), "XFCE")) {
+ name = "xfce";
+ } else if (strstr(desktop.c_str(), "MATE")) {
+ name = "mate";
+ }
+
+ if (!name.empty()) {
+ size_t pos;
+ std::string replace_word = "freedesktop";
+ pos = dbus_service.find(replace_word);
+ dbus_service.replace(pos, replace_word.size(), name);
+ pos = dbus_path.find(replace_word);
+ dbus_path.replace(pos, replace_word.size(), name);
+ pos = dbus_interface.find(replace_word);
+ dbus_interface.replace(pos, replace_word.size(), name);
+ }
+
+ LOGI("Will attempt to use %s (D-Bus screensaver inhibition) %s", dbus_service.c_str(),
+ (scrsv == 1 ? "only during screen activity" : "always"));
+ if (scrsv == 2) {
+ dbus_screensaver_inhibiter(true);
+ }
+ }
+#endif
if (audiosink == "0") {
use_audio = false;
dump_audio = false;
@@ -2667,4 +2847,17 @@ int main (int argc, char *argv[]) {
if (metadata_filename.length()) {
remove (metadata_filename.c_str());
}
+#ifdef DBUS
+ if (dbus_connection) {
+ LOGD("Ending D-Bus connection %p", dbus_connection);
+ if (dbus_last_message) {
+ dbus_screensaver_inhibiter(false);
+ }
+ if (dbus_pending) {
+ dbus_pending_call_cancel(dbus_pending);
+ dbus_pending_call_unref(dbus_pending);
+ }
+ dbus_connection_unref(dbus_connection);
+ }
+#endif
}
From 483399a713e21eadc81235902b58d97a1af1e0d9 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 12 Aug 2025 18:22:26 -0400
Subject: [PATCH 15/58] possible fix for failure to print a plist
---
lib/http_handlers.h | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/lib/http_handlers.h b/lib/http_handlers.h
index 1c17717..02ebcba 100644
--- a/lib/http_handlers.h
+++ b/lib/http_handlers.h
@@ -447,19 +447,30 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
printf("\n\n%s\n", header_str);
- bool is_plist = (bool) strstr(header_str,"apple-binary-plist");
+ bool data_is_plist = (strstr(header_str,"apple-binary-plist") != NULL);
free(header_str);
- if (is_plist) {
+ if (data_is_plist) {
int request_datalen;
const char *request_data = http_request_get_data(request, &request_datalen);
plist_t req_root_node = NULL;
plist_from_bin(request_data, request_datalen, &req_root_node);
- char * plist_xml;
- uint32_t plist_len;
+ char *plist_xml = NULL;
+ char *stripped_xml = NULL;
+ uint32_t plist_len = 0;
plist_to_xml(req_root_node, &plist_xml, &plist_len);
- plist_xml = utils_strip_data_from_plist_xml(plist_xml);
- printf("%s", plist_xml);
- free(plist_xml);
+ printf("plist_len = %u\n", plist_len);
+ stripped_xml = utils_strip_data_from_plist_xml(plist_xml);
+ printf("%s", stripped_xml ? stripped_xml : plist_xml);
+ if (stripped_xml) {
+ free(stripped_xml);
+ }
+ if (plist_xml) {
+#ifdef PLIST_230
+ plist_mem_free(plist_xml);
+#else
+ plist_to_xml_free(plist_xml);
+#endif
+ }
plist_free(req_root_node);
}
assert(0);
From 1e282eeb2207b2ebf5bf43e126fe2f166d87d1a1 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 15 Aug 2025 03:24:38 -0400
Subject: [PATCH 16/58] test for AirMyPC without version number,
---
lib/raop_handlers.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 1e7649b..0837e40 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -765,6 +765,7 @@ raop_handler_setup(raop_conn_t *conn,
bool old_protocol = false;
#ifdef OLD_PROTOCOL_CLIENT_USER_AGENT_LIST /* set in global.h */
if (strstr(OLD_PROTOCOL_CLIENT_USER_AGENT_LIST, user_agent)) old_protocol = true;
+ if (strstr(user_agent, "AirMyPC")) old_protocol = true; //AirMyPC/7200 still uses old protocol: unlikely to change (?)
#endif
if (old_protocol) { /* some windows AirPlay-client emulators use old AirPlay 1 protocol with unhashed AES key */
logger_log(conn->raop->logger, LOGGER_INFO, "Client identifed as using old protocol (unhashed) AES audio key)");
From b3679fd630cd529cc0da1786dfe9a89816ea3ba4 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 15 Aug 2025 14:23:05 -0400
Subject: [PATCH 17/58] discard payload_size=12 no-data audio packets (seen in
AirMyPC)
---
lib/raop_rtp.c | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/lib/raop_rtp.c b/lib/raop_rtp.c
index 85f7de7..e76e90f 100644
--- a/lib/raop_rtp.c
+++ b/lib/raop_rtp.c
@@ -429,6 +429,19 @@ raop_rtp_thread_udp(void *arg)
logger_log(raop_rtp->logger, LOGGER_DEBUG, "raop_rtp start_time = %8.6f (raop_rtp audio)",
((double) raop_rtp->ntp_start_time) / SEC);
+ char type[8] = {'\0'};
+ switch (raop_rtp->ct) {
+ case 2:
+ snprintf(type, 8, "ALAC");
+ break;
+ case 8:
+ snprintf(type, 8, "AAC_ELD");
+ break;
+ default:
+ snprintf(type, 8, "TYPE=?");
+ break;
+ }
+
while(1) {
fd_set rfds;
struct timeval tv;
@@ -605,7 +618,7 @@ raop_rtp_thread_udp(void *arg)
raop_rtp->initial_sync = true;
}
- if (packetlen == 16 && memcmp(packet + 12, no_data_marker, 4) == 0) {
+ if (packetlen == 12 ||(packetlen == 16 && memcmp(packet + 12, no_data_marker, 4) == 0)) {
/* this is a "no data" packet */
/* the first such packet could be used to provide the initial rtptime and seqnum formerly given in the RECORD request */
continue;
@@ -640,9 +653,9 @@ raop_rtp_thread_udp(void *arg)
uint64_t ntp_now = raop_ntp_get_local_time();
int64_t latency = (audio_data.ntp_time_local ? ((int64_t) ntp_now) - ((int64_t) audio_data.ntp_time_local) : 0);
logger_log(raop_rtp->logger, LOGGER_DEBUG,
- "raop_rtp audio: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, rtp_time=%u seqnum = %u",
+ "raop_rtp audio: now = %8.6f, ntp = %8.6f, latency = %9.6f, ts = %8.6f, rtp_time=%u seqnum = %5u %s %d",
(double) ntp_now / SEC, (double) audio_data.ntp_time_local / SEC, (double) latency / SEC,
- (double) audio_data.ntp_time_remote /SEC, rtp_timestamp, seqnum);
+ (double) audio_data.ntp_time_remote /SEC, rtp_timestamp, seqnum, type, payload_size);
}
raop_rtp->callbacks.audio_process(raop_rtp->callbacks.cls, raop_rtp->ntp, &audio_data);
From 14463c04a8f256c10a113a3b524d36037d37d30c Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 24 Aug 2025 14:04:41 -0400
Subject: [PATCH 18/58] code cleanup video_renderer.c
---
renderers/video_renderer.c | 41 +++++++++++++++++++++++++++-----------
1 file changed, 29 insertions(+), 12 deletions(-)
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 979c53f..0f64183 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -54,6 +54,11 @@ static gboolean hls_seek_enabled;
static gboolean hls_playing;
static gboolean hls_buffer_empty;
static gboolean hls_buffer_full;
+static int type_264;
+static int type_265;
+static int type_hls;
+static int type_jpeg;
+
typedef enum {
@@ -238,6 +243,10 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
hls_duration = -1;
hls_buffer_empty = TRUE;
hls_buffer_empty = FALSE;
+ type_hls = -1;
+ type_264 = -1;
+ type_265 = -1;
+ type_jpeg = -1;
/* this call to g_set_application_name makes server_name appear in the X11 display window title bar, */
@@ -284,6 +293,7 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
logger_log(logger, LOGGER_INFO, "Will use GStreamer playbin version %u to play HLS streamed video", playbin_version);
g_assert(renderer_type[i]->pipeline);
renderer_type[i]->codec = hls;
+ type_hls = i;
/* if we are not using an autovideosink, build a videosink based on the string "videosink" */
if (!auto_videosink) {
GstElement *playbin_videosink = make_video_sink(videosink, videosink_options);
@@ -307,14 +317,17 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
jpeg_pipeline = true;
renderer_type[i]->codec = jpeg;
caps = gst_caps_from_string(jpeg_caps);
+ type_jpeg = i;
break;
case 1:
renderer_type[i]->codec = h264;
caps = gst_caps_from_string(h264_caps);
+ type_264 = i;
break;
case 2:
renderer_type[i]->codec = h265;
caps = gst_caps_from_string(h265_caps);
+ type_265 = i;
break;
default:
g_assert(0);
@@ -582,9 +595,9 @@ void video_renderer_stop() {
static void video_renderer_destroy_instance(video_renderer_t *renderer) {
if (renderer) {
- logger_log(logger, LOGGER_DEBUG,"destroying renderer instance %p", renderer);
+ logger_log(logger, LOGGER_DEBUG,"destroying renderer instance %p codec=%s ", renderer, renderer->codec);
GstState state;
- GstStateChangeReturn ret;
+ GstStateChangeReturn ret;
gst_element_get_state(renderer->pipeline, &state, NULL, 100 * GST_MSECOND);
logger_log(logger, LOGGER_DEBUG,"pipeline state is %s", gst_element_state_get_name(state));
if (state != GST_STATE_NULL) {
@@ -884,17 +897,21 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
video_renderer_t *renderer_used = NULL;
g_assert(!hls_video);
if (video_is_jpeg) {
- renderer_used = renderer_type[0];
- } else if (n_renderers == 2) {
- if (video_is_h265) {
- logger_log(logger, LOGGER_ERR, "video is h265 but the -h265 option was not used");
- return -1;
- }
- renderer_used = renderer_type[1];
+ g_assert(type_jpeg != -1);
+ renderer_used = renderer_type[type_jpeg];
} else {
- renderer_used = video_is_h265 ? renderer_type[2] : renderer_type[1];
+ if (video_is_h265) {
+ if (type_265 == -1) {
+ logger_log(logger, LOGGER_ERR, "video is h265 but the -h265 option was not used");
+ return -1;
+ }
+ renderer_used = renderer_type[type_265];
+ } else {
+ g_assert(type_264 != -1);
+ renderer_used = renderer_type[type_264];
+ }
}
- if (renderer_used == NULL) {
+ if (renderer_used == NULL) {
return -1;
} else if (renderer_used == renderer) {
return 0;
@@ -911,7 +928,7 @@ int video_renderer_choose_codec (bool video_is_jpeg, bool video_is_h265) {
logger_log(logger, LOGGER_DEBUG, "video_pipeline state change from %s to %s\n",
gst_element_state_get_name (old_state),gst_element_state_get_name (new_state));
gst_video_pipeline_base_time = gst_element_get_base_time(renderer->appsrc);
- if (n_renderers > 2 && renderer == renderer_type[2]) {
+ if (strstr(renderer->codec, h265)) {
logger_log(logger, LOGGER_INFO, "*** video format is h265 high definition (HD/4K) video %dx%d", width, height);
}
/* destroy unused renderers */
From e0ee600e3e091634d07d0386c01fca21cfee1cdf Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Mon, 25 Aug 2025 01:41:12 -0400
Subject: [PATCH 19/58] whitespace httpd.c
---
lib/httpd.c | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/lib/httpd.c b/lib/httpd.c
index bfc554b..8af6f74 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -344,7 +344,7 @@ httpd_thread(void *arg)
struct timeval tv;
int nfds=0;
int ret;
- int new_request;
+ int new_request;
MUTEX_LOCK(httpd->run_mutex);
if (!httpd->running) {
@@ -432,11 +432,11 @@ httpd_thread(void *arg)
if (connection->type == CONNECTION_TYPE_PTTH) {
http_request_is_reverse(connection->request);
}
- logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
+ logger_log(httpd->logger, LOGGER_DEBUG, "new request, connection %d, socket %d type %s",
i, connection->socket_fd, typename [connection->type]);
} else {
new_request = 0;
- }
+ }
logger_log(httpd->logger, LOGGER_DEBUG, "httpd receiving on socket %d, connection %d",
connection->socket_fd, i);
@@ -444,20 +444,20 @@ httpd_thread(void *arg)
logger_log(httpd->logger, LOGGER_DEBUG,"\nhttpd: current connections:");
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
- if(!connection->connected) {
+ if (!connection->connected) {
continue;
}
if (!FD_ISSET(connection->socket_fd, &rfds)) {
logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s", i,
connection->type, connection->socket_fd,
connection->user_data, typename [connection->type]);
- } else {
- logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
- i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
+ } else {
+ logger_log(httpd->logger, LOGGER_DEBUG, "connection %d type %d socket %d conn %p %s ACTIVE CONNECTION",
+ i, connection->type, connection->socket_fd, connection->user_data, typename [connection->type]);
}
}
- logger_log(httpd->logger, LOGGER_DEBUG, " ");
- }
+ logger_log(httpd->logger, LOGGER_DEBUG, " ");
+ }
/* reverse-http responses from the client must not be sent to the llhttp parser:
* such messages start with "HTTP/1.1" */
if (new_request) {
@@ -470,14 +470,14 @@ httpd_thread(void *arg)
connection->socket_fd);
break;
} else if (ret == -1) {
- if (errno == EAGAIN) {
- continue;
- } else {
- int sock_err = SOCKET_GET_ERROR();
- logger_log(httpd->logger, LOGGER_ERR, "httpd: recv socket error %d:%s",
- sock_err, SOCKET_ERROR_STRING(sock_err));
- break;
- }
+ if (errno == EAGAIN) {
+ continue;
+ } else {
+ int sock_err = SOCKET_GET_ERROR();
+ logger_log(httpd->logger, LOGGER_ERR, "httpd: recv socket error %d:%s",
+ sock_err, SOCKET_ERROR_STRING(sock_err));
+ break;
+ }
} else {
readstart += ret;
ret = readstart;
From 7bab0f010b206c73d35940b786ba2da4ee0b7cfb Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 26 Aug 2025 17:09:13 -0400
Subject: [PATCH 20/58] httpd.c: include socket number in error message
---
lib/httpd.c | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/lib/httpd.c b/lib/httpd.c
index 8af6f74..ae85c01 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -463,10 +463,10 @@ httpd_thread(void *arg)
if (new_request) {
int readstart = 0;
new_request = 0;
- while (readstart < 8) {
+ while (readstart < 8 && connection->socket_fd) {
ret = recv(connection->socket_fd, buffer + readstart, sizeof(buffer) - 1 - readstart, 0);
if (ret == 0) {
- logger_log(httpd->logger, LOGGER_DEBUG, "client closed connection on socket %d",
+ logger_log(httpd->logger, LOGGER_DEBUG, "client closed connection on socket %d",
connection->socket_fd);
break;
} else if (ret == -1) {
@@ -474,8 +474,8 @@ httpd_thread(void *arg)
continue;
} else {
int sock_err = SOCKET_GET_ERROR();
- logger_log(httpd->logger, LOGGER_ERR, "httpd: recv socket error %d:%s",
- sock_err, SOCKET_ERROR_STRING(sock_err));
+ logger_log(httpd->logger, LOGGER_ERR, "httpd: recv error %d on socket %d: %s",
+ sock_err, connection->socket_fd, SOCKET_ERROR_STRING(sock_err));
break;
}
} else {
@@ -483,13 +483,22 @@ httpd_thread(void *arg)
ret = readstart;
}
}
+ if (!connection->socket_fd) {
+ /* connection was recently removed */
+ continue;
+ }
if (!memcmp(buffer, http, 8)) {
http_request_set_reverse(connection->request);
}
} else {
- ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
- if (ret == 0) {
- httpd_remove_connection(httpd, connection);
+ if (connection->socket_fd) {
+ ret = recv(connection->socket_fd, buffer, sizeof(buffer) - 1, 0);
+ if (ret == 0) {
+ httpd_remove_connection(httpd, connection);
+ continue;
+ }
+ } else {
+ /* connection was recently removed */
continue;
}
}
From 1088443c67a4dbeb93078ff3b7c33c8d7912d233 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Thu, 28 Aug 2025 23:01:31 -0400
Subject: [PATCH 21/58] only build jpeg renderer if --ca (no filename) is used
---
renderers/video_renderer.c | 46 +++++++++++++++++---------------------
renderers/video_renderer.h | 3 ++-
uxplay.cpp | 29 ++++++++++++++++--------
3 files changed, 43 insertions(+), 35 deletions(-)
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 0f64183..ca1791f 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -59,8 +59,6 @@ static int type_265;
static int type_hls;
static int type_jpeg;
-
-
typedef enum {
//GST_PLAY_FLAG_VIDEO = (1 << 0),
//GST_PLAY_FLAG_AUDIO = (1 << 1),
@@ -226,7 +224,7 @@ GstElement *make_video_sink(const char *videosink, const char *videosink_options
void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
- bool initial_fullscreen, bool video_sync, bool h265_support, guint playbin_version, const char *uri) {
+ bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support, guint playbin_version, const char *uri) {
GError *error = NULL;
GstCaps *caps = NULL;
hls_video = (uri != NULL);
@@ -247,7 +245,6 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
type_264 = -1;
type_265 = -1;
type_jpeg = -1;
-
/* this call to g_set_application_name makes server_name appear in the X11 display window title bar, */
/* (instead of the program name uxplay taken from (argv[0]). It is only set one time. */
@@ -255,18 +252,22 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
const gchar *appname = g_get_application_name();
if (!appname || strcmp(appname,server_name)) g_set_application_name(server_name);
appname = NULL;
-
+ n_renderers = 1;
/* the renderer for hls video will only be built if a HLS uri is provided in
* the call to video_renderer_init, in which case the h264/h265 mirror-mode and jpeg
* audio-mode renderers will not be built. This is because it appears that we cannot
* put playbin into GST_STATE_READY before knowing the uri (?), so cannot use a
* unified renderer structure with h264, h265, jpeg and hls */
if (hls_video) {
- n_renderers = 1;
- /* renderer[0]: playbin (hls) */
+ type_hls = 0;
} else {
- n_renderers = h265_support ? 3 : 2;
- /* renderer[0]: jpeg; [1]: h264; [2]: h265 */
+ type_264 = 0;
+ if (h265_support) {
+ type_265 = n_renderers++;
+ }
+ if (coverart_support) {
+ type_jpeg = n_renderers++;
+ }
}
g_assert (n_renderers <= NCODECS);
for (int i = 0; i < n_renderers; i++) {
@@ -293,7 +294,6 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
logger_log(logger, LOGGER_INFO, "Will use GStreamer playbin version %u to play HLS streamed video", playbin_version);
g_assert(renderer_type[i]->pipeline);
renderer_type[i]->codec = hls;
- type_hls = i;
/* if we are not using an autovideosink, build a videosink based on the string "videosink" */
if (!auto_videosink) {
GstElement *playbin_videosink = make_video_sink(videosink, videosink_options);
@@ -312,24 +312,17 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
g_object_set (G_OBJECT (renderer_type[i]->pipeline), "uri", uri, NULL);
} else {
bool jpeg_pipeline = false;
- switch (i) {
- case 0:
+ if (i == type_264) {
+ renderer_type[i]->codec = h264;
+ caps = gst_caps_from_string(h264_caps);
+ } else if (i == type_265) {
+ renderer_type[i]->codec = h265;
+ caps = gst_caps_from_string(h265_caps);
+ } else if (i == type_jpeg) {
jpeg_pipeline = true;
renderer_type[i]->codec = jpeg;
caps = gst_caps_from_string(jpeg_caps);
- type_jpeg = i;
- break;
- case 1:
- renderer_type[i]->codec = h264;
- caps = gst_caps_from_string(h264_caps);
- type_264 = i;
- break;
- case 2:
- renderer_type[i]->codec = h265;
- caps = gst_caps_from_string(h265_caps);
- type_265 = i;
- break;
- default:
+ } else {
g_assert(0);
}
GString *launch = g_string_new("appsrc name=video_source ! ");
@@ -516,6 +509,9 @@ bool waiting_for_x11_window() {
void video_renderer_display_jpeg(const void *data, int *data_len) {
GstBuffer *buffer;
+ if (type_jpeg == -1) {
+ return;
+ }
if (renderer && !strcmp(renderer->codec, jpeg)) {
buffer = gst_buffer_new_allocate(NULL, *data_len, NULL);
g_assert(buffer != NULL);
diff --git a/renderers/video_renderer.h b/renderers/video_renderer.h
index 9c307e8..5a0084f 100644
--- a/renderers/video_renderer.h
+++ b/renderers/video_renderer.h
@@ -49,7 +49,8 @@ typedef struct video_renderer_s video_renderer_t;
void video_renderer_init (logger_t *logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
- bool initial_fullscreen, bool video_sync, bool h265_support, guint playbin_version, const char *uri);
+ bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support,
+ guint playbin_version, const char *uri);
void video_renderer_start ();
void video_renderer_stop ();
void video_renderer_pause ();
diff --git a/uxplay.cpp b/uxplay.cpp
index dc1e1ea..64deac2 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -558,34 +558,43 @@ static guint g_unix_signal_add(gint signum, GSourceFunc handler, gpointer user_d
}
#endif
+#define MAX_VIDEO_RENDERERS 3
+#define MAX_AUDIO_RENDERERS 2
static void main_loop() {
- guint gst_video_bus_watch_id[3] = { 0 };
- g_assert(n_video_renderers <= 3);
- guint gst_audio_bus_watch_id[2] = { 0 };
- g_assert(n_audio_renderers <= 2);
+ guint gst_video_bus_watch_id[MAX_VIDEO_RENDERERS] = { 0 };
+ guint gst_audio_bus_watch_id[MAX_AUDIO_RENDERERS] = { 0 };
GMainLoop *loop = g_main_loop_new(NULL,FALSE);
relaunch_video = false;
reset_loop = false;
reset_httpd = false;
preserve_connections = false;
+ n_video_renderers = 0;
+ n_audio_renderers = 0;
if (use_video) {
+ n_video_renderers = 1;
relaunch_video = true;
if (url.empty()) {
- /* renderer[0] : jpeg coverart; renderer[1] h264 video; renderer[2] h265 video (optional) */
- n_video_renderers = h265_support ? 3 : 2;
+ if (h265_support) {
+ n_video_renderers++;
+ }
+ if (render_coverart) {
+ n_video_renderers++;
+ }
+ /* renderer[0] : h264 video; followed by h265 video (optional) and jpeg (optional) */
gst_x11_window_id = 0;
} else {
/* hls video will be rendered: renderer[0] : hls */
- n_video_renderers = 1;
url.erase();
gst_x11_window_id = g_timeout_add(100, (GSourceFunc) x11_window_callback, (gpointer) loop);
}
+ g_assert(n_video_renderers <= MAX_VIDEO_RENDERERS);
for (int i = 0; i < n_video_renderers; i++) {
gst_video_bus_watch_id[i] = (guint) video_renderer_listen((void *)loop, i);
}
}
if (use_audio) {
n_audio_renderers = 2;
+ g_assert(n_audio_renderers <= MAX_AUDIO_RENDERERS);
for (int i = 0; i < n_audio_renderers; i++) {
gst_audio_bus_watch_id[i] = (guint) audio_renderer_listen((void *)loop, i);
}
@@ -2727,7 +2736,8 @@ int main (int argc, char *argv[]) {
if (use_video) {
video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
- videosink_options.c_str(), fullscreen, video_sync, h265_support, playbin_version, NULL);
+ videosink_options.c_str(), fullscreen, video_sync, h265_support,
+ render_coverart, playbin_version, NULL);
video_renderer_start();
#ifdef __OpenBSD__
} else {
@@ -2811,7 +2821,8 @@ int main (int argc, char *argv[]) {
const char *uri = (url.empty() ? NULL : url.c_str());
video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
- videosink_options.c_str(), fullscreen, video_sync, h265_support, playbin_version, uri);
+ videosink_options.c_str(), fullscreen, video_sync, h265_support,
+ render_coverart, playbin_version, uri);
video_renderer_start();
}
if (reset_httpd) {
From 5084d2f8e74af75429e1ab730b81b016c63a2d29 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 30 Aug 2025 10:20:30 -0400
Subject: [PATCH 22/58] rearrange order in teardown_handler to avoid race
---
lib/raop_handlers.h | 17 ++++++-----------
1 file changed, 6 insertions(+), 11 deletions(-)
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 0837e40..fdee6a7 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -1174,11 +1174,6 @@ raop_handler_teardown(raop_conn_t *conn,
data = http_request_get_data(request, &data_len);
plist_t req_root_node = NULL;
plist_from_bin(data, data_len, &req_root_node);
- char * plist_xml;
- uint32_t plist_len;
- plist_to_xml(req_root_node, &plist_xml, &plist_len);
- logger_log(conn->raop->logger, LOGGER_DEBUG, "%s", plist_xml);
- free(plist_xml);
plist_t req_streams_node = plist_dict_get_item(req_root_node, "streams");
/* Process stream teardown requests */
if (PLIST_IS_ARRAY(req_streams_node)) {
@@ -1196,9 +1191,6 @@ raop_handler_teardown(raop_conn_t *conn,
}
}
plist_free(req_root_node);
- if (conn->raop->callbacks.conn_teardown) {
- conn->raop->callbacks.conn_teardown(conn->raop->callbacks.cls, &teardown_96, &teardown_110);
- }
logger_log(conn->raop->logger, LOGGER_DEBUG, "TEARDOWN request, 96=%d, 110=%d", teardown_96, teardown_110);
http_response_add_header(response, "Connection", "close");
@@ -1228,7 +1220,10 @@ raop_handler_teardown(raop_conn_t *conn,
raop_rtp_mirror_destroy(conn->raop_rtp_mirror);
conn->raop_rtp_mirror = NULL;
}
- /* shut down any HLS connections */
- httpd_remove_connections_by_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
- }
+ /* shut down any HLS connections */
+ httpd_remove_connections_by_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
+ }
+ if (conn->raop->callbacks.conn_teardown) {
+ conn->raop->callbacks.conn_teardown(conn->raop->callbacks.cls, &teardown_96, &teardown_110);
+ }
}
From 94a7733d7e840dd219d851a4e36cbd0eb848010e Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 30 Aug 2025 13:27:39 -0400
Subject: [PATCH 23/58] handle POST audioMode RTSP request
---
lib/raop.c | 4 +---
lib/raop_handlers.h | 21 +++++++++++++++++++++
2 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/lib/raop.c b/lib/raop.c
index 95700f6..80bb996 100644
--- a/lib/raop.c
+++ b/lib/raop.c
@@ -363,10 +363,8 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &raop_handler_pairverify;
} else if (!strcmp(url, "/fp-setup")) {
handler = &raop_handler_fpsetup;
- } else if (!strcmp(url, "/getProperty")) {
- handler = &http_handler_get_property;
} else if (!strcmp(url, "/audioMode")) {
- //handler = &http_handler_audioMode;
+ handler = &raop_handler_audiomode;
}
} else if (!strcmp(method, "GET")) {
if (!strcmp(url, "/info")) {
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index fdee6a7..4d7af19 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -1116,6 +1116,27 @@ raop_handler_set_parameter(raop_conn_t *conn,
}
}
+static void
+raop_handler_audiomode(raop_conn_t *conn,
+ http_request_t *request, http_response_t *response,
+ char **response_data, int *response_datalen)
+{
+ const char *data = NULL;
+ char *audiomode = NULL;
+ int data_len;
+ data = http_request_get_data(request, &data_len);
+ plist_t req_root_node = NULL;
+ plist_from_bin(data, data_len, &req_root_node);
+ plist_t req_audiomode_node = plist_dict_get_item(req_root_node, "audioMode");
+ plist_get_string_val(req_audiomode_node, &audiomode);
+ /* not sure what should be done with this request: usually audioMode requested is "default" */
+ int log_level = (strstr(audiomode, "default") ? LOGGER_DEBUG : LOGGER_INFO);
+ logger_log(conn->raop->logger, log_level, "Unhandled RTSP request \"audioMode: %s\"", audiomode);
+ if (audiomode) {
+ free(audiomode);
+ }
+ plist_free(req_root_node);
+}
static void
raop_handler_feedback(raop_conn_t *conn,
From 8ff22f17ac2a072ba20d059890ed756f16978d40 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 30 Aug 2025 15:14:58 -0400
Subject: [PATCH 24/58] httpd.c: avoid race giving socket errors when
connection is closed
---
lib/httpd.c | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/lib/httpd.c b/lib/httpd.c
index ae85c01..f50a74b 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -201,25 +201,26 @@ httpd_destroy(httpd_t *httpd)
static void
httpd_remove_connection(httpd_t *httpd, http_connection_t *connection)
{
+ int socket_fd = connection->socket_fd;
+ connection->socket_fd = 0;
if (connection->request) {
http_request_destroy(connection->request);
connection->request = NULL;
}
logger_log(httpd->logger, LOGGER_DEBUG, "removing connection type %s socket %d conn %p", typename[connection->type],
- connection->socket_fd, connection->user_data);
+ socket_fd, connection->user_data);
if (connection->user_data) {
httpd->callbacks.conn_destroy(connection->user_data);
connection->user_data = NULL;
}
- if (connection->socket_fd) {
- shutdown(connection->socket_fd, SHUT_WR);
- int ret = closesocket(connection->socket_fd);
+ if (socket_fd) {
+ shutdown(socket_fd, SHUT_WR);
+ int ret = closesocket(socket_fd);
if (ret == -1) {
logger_log(httpd->logger, LOGGER_ERR, "httpd error in closesocket (close): %d %s", errno, strerror(errno));
} else {
- logger_log(httpd->logger, LOGGER_INFO, "Connection closed on socket %d", connection->socket_fd);
+ logger_log(httpd->logger, LOGGER_INFO, "Connection closed on socket %d", socket_fd);
}
- connection->socket_fd = 0;
}
if (connection->connected) {
connection->connected = 0;
@@ -444,7 +445,7 @@ httpd_thread(void *arg)
logger_log(httpd->logger, LOGGER_DEBUG,"\nhttpd: current connections:");
for (int i = 0; i < httpd->max_connections; i++) {
http_connection_t *connection = &httpd->connections[i];
- if (!connection->connected) {
+ if (!connection->connected || !connection->socket_fd) {
continue;
}
if (!FD_ISSET(connection->socket_fd, &rfds)) {
@@ -463,7 +464,10 @@ httpd_thread(void *arg)
if (new_request) {
int readstart = 0;
new_request = 0;
- while (readstart < 8 && connection->socket_fd) {
+ while (readstart < 8) {
+ if (!connection->socket_fd) {
+ break;
+ }
ret = recv(connection->socket_fd, buffer + readstart, sizeof(buffer) - 1 - readstart, 0);
if (ret == 0) {
logger_log(httpd->logger, LOGGER_DEBUG, "client closed connection on socket %d",
From b682eea3237f7dc0d2b57df740db8185d41db8c8 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 30 Aug 2025 18:01:42 -0400
Subject: [PATCH 25/58] remove the d3d12->d3d11 workaround (d3d12 is now
fixed)
---
README.html | 9 +++++----
README.md | 9 +++++----
README.txt | 11 ++++++-----
uxplay.1 | 4 +++-
uxplay.cpp | 21 ++++-----------------
5 files changed, 23 insertions(+), 31 deletions(-)
diff --git a/README.html b/README.html
index b98ce60..e40a0b3 100644
--- a/README.html
+++ b/README.html
@@ -999,10 +999,11 @@ used.
<videosink> are d3d12videosink,
d3d11videosink, d3dvideosink,
glimagesink, gtksink,
-autovideosink. If you do not specify the videosink, the
-d3d11videosink will be used (users have reported segfaults of the newer
-d3d12 videodecoder on certain older Nvidia cards when the image
-resolution changes: d3d11 will used by default until this is fixed).
+autovideosink. There have been reports of segfaults of
+the newer d3d12 videodecoder on certain older Nvidia cards when the
+image resolution changes, e.g., when the iOS client is rotated between
+portrait and landcape modes: this was a GStreamer issue that is
+apparently now fixed (a workaround is to use d3d11).
- With Direct3D 11.0 or greater, various options can be set using
e.g.
-vs "d3d11videosink <options>" (see the
diff --git a/README.md b/README.md
index 03f15b8..7ee7542 100644
--- a/README.md
+++ b/README.md
@@ -987,10 +987,11 @@ like `\{0.0.0.00000000\}.\{98e35b2b-8eba-412e-b840-fd2c2492cf44\}`. If
If you wish to specify the videosink using the `-vs ` option,
some choices for `` are `d3d12videosink`, ``d3d11videosink``, ```d3dvideosink```,
-`glimagesink`, ``gtksink``, ```autovideosink```. If you do not specify the videosink,
-the d3d11videosink will be used (users have reported segfaults of the newer d3d12 videodecoder
-on certain older Nvidia cards when the image resolution changes:
-d3d11 will used by default until this is fixed).
+`glimagesink`, ``gtksink``, ```autovideosink```. _There have been reports of
+segfaults of the newer d3d12 videodecoder
+on certain older Nvidia cards when the image resolution changes, e.g., when the iOS client
+is rotated between portrait and landcape modes: this was a GStreamer issue
+that is apparently now fixed (a workaround is to use d3d11)._
- With Direct3D 11.0 or greater, various options can be set
using e.g. `-vs "d3d11videosink "` (see the gstreamer videosink
diff --git a/README.txt b/README.txt
index 65e295f..e06690f 100644
--- a/README.txt
+++ b/README.txt
@@ -1011,11 +1011,12 @@ like `\{0.0.0.00000000\}.\{98e35b2b-8eba-412e-b840-fd2c2492cf44\}`. If
If you wish to specify the videosink using the `-vs ` option,
some choices for `` are `d3d12videosink`, `d3d11videosink`,
-`d3dvideosink`, `glimagesink`, `gtksink`, `autovideosink`. If you do not
-specify the videosink, the d3d11videosink will be used (users have
-reported segfaults of the newer d3d12 videodecoder on certain older
-Nvidia cards when the image resolution changes: d3d11 will used by
-default until this is fixed).
+`d3dvideosink`, `glimagesink`, `gtksink`, `autovideosink`. *There have
+been reports of segfaults of the newer d3d12 videodecoder on certain
+older Nvidia cards when the image resolution changes, e.g., when the iOS
+client is rotated between portrait and landcape modes: this was a
+GStreamer issue that is apparently now fixed (a workaround is to use
+d3d11).*
- With Direct3D 11.0 or greater, various options can be set using
e.g. `-vs "d3d11videosink "` (see the gstreamer videosink
diff --git a/uxplay.1 b/uxplay.1
index 0c429bb..237d782 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -91,7 +91,9 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.IP
choices: ximagesink,xvimagesink,vaapisink,glimagesink,
.IP
- gtksink,waylandsink,osxvideosink,kmssink,d3d11videosink,...
+ gtksink,waylandsink,kmssink,fbdevsink,osxvideosink,
+.IP
+ d3d11videosink,d3d12videosink ...
.PP
.TP
\fB\-vs\fR 0 Streamed audio only, with no video display window.
diff --git a/uxplay.cpp b/uxplay.cpp
index 64deac2..459e3f1 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -806,7 +806,7 @@ static void print_info (char *name) {
printf("-s wxh[@r]Request to client for video display resolution [refresh_rate]\n");
printf(" default 1920x1080[@60] (or 3840x2160[@60] with -h265 option)\n");
printf("-o Set display \"overscanned\" mode on (not usually needed)\n");
- printf("-fs Full-screen (only with X11, Wayland, VAAPI, D3D11, kms)\n");
+ printf("-fs Full-screen (only with X11, Wayland, VAAPI, D3D11/12, kms)\n");
printf("-p Use legacy ports UDP 6000:6001:7011 TCP 7000:7001:7100\n");
printf("-p n Use TCP and UDP ports n,n+1,n+2. range %d-%d\n", LOWEST_ALLOWED_PORT, HIGHEST_PORT);
printf(" use \"-p n1,n2,n3\" to set each port, \"n1,n2\" for n3 = n2+1\n");
@@ -821,7 +821,8 @@ static void print_info (char *name) {
printf(" another choice when using v4l2h264dec: v4l2convert\n");
printf("-vs ... Choose the GStreamer videosink; default \"autovideosink\"\n");
printf(" some choices: ximagesink,xvimagesink,vaapisink,glimagesink,\n");
- printf(" gtksink,waylandsink,osxvideosink,kmssink,d3d11videosink etc.\n");
+ printf(" gtksink,waylandsink,kmssink,fbdevsink,osxvideosink,\n");
+ printf(" d3d11videosink,d3v12videosink, etc.\n");
printf("-vs 0 Streamed audio only, with no video display window\n");
printf("-v4l2 Use Video4Linux2 for GPU hardware h264 decoding\n");
printf("-bt709 Sometimes needed for Raspberry Pi models using Video4Linux2 \n");
@@ -2608,20 +2609,6 @@ int main (int argc, char *argv[]) {
}
#endif
-#ifdef _WIN32
- /* because of issues in videosink dvd312videosink (segfault when resolution changes
- with certain Nvdia graphics cards) make the default videosink d3d11videosink, and
- use its decoder */
- if (videosink == "autovideosink") {
- videosink.erase();
- videosink.append("d3d11videosink");
- }
- if (videosink == "d3d11videosink") {
- video_decoder.erase();
- video_decoder.append("d3d11h264dec");
- }
-#endif
-
if (videosink == "0") {
use_video = false;
videosink.erase();
@@ -2635,7 +2622,7 @@ int main (int argc, char *argv[]) {
if (videosink == "waylandsink" || videosink == "vaapisink") {
videosink_options.append(" fullscreen=true");
} else if (videosink == "kmssink") {
- videosink_options.append(" force_modesetting=true");
+ videosink_options.append(" force_modesetting=true");
}
}
From 784f2887e38229889bb819fb266cf817050cb889 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 30 Aug 2025 18:31:59 -0400
Subject: [PATCH 26/58] httpd.c: add support for Windows socket error strings
---
lib/httpd.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lib/httpd.c b/lib/httpd.c
index f50a74b..97b1cbd 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -391,7 +391,9 @@ httpd_thread(void *arg)
/* Timeout happened */
continue;
} else if (ret == -1) {
- logger_log(httpd->logger, LOGGER_ERR, "httpd error in select: %d %s", errno, strerror(errno));
+ int sock_err = SOCKET_GET_ERROR();
+ logger_log(httpd->logger, LOGGER_ERR,
+ "httpd error in select: %d %s", sock_err, SOCKET_ERROR_STRING(sock_err));
break;
}
From 502582464b3be9bc31767189abb8069f12855d73 Mon Sep 17 00:00:00 2001
From: oogabooga <73903616+paulohardy@users.noreply.github.com>
Date: Sun, 31 Aug 2025 04:02:56 +0300
Subject: [PATCH 27/58] specfile build fix
---
uxplay.spec | 1 +
1 file changed, 1 insertion(+)
diff --git a/uxplay.spec b/uxplay.spec
index e302bb4..f484518 100644
--- a/uxplay.spec
+++ b/uxplay.spec
@@ -129,6 +129,7 @@ cd build
%{_docdir}/%{name}/README.txt
%{_docdir}/%{name}/README.html
%{_docdir}/%{name}/README.md
+%{_docdir}/%{name}/systemd/uxplay.service
%license
%{_docdir}/%{name}/LICENSE
From 738866a62561046f5ab2b3881cca4c35cde5f171 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 31 Aug 2025 02:22:50 -0400
Subject: [PATCH 28/58] improve audio progress report with 1 sec updating
---
lib/raop.h | 2 +-
lib/raop_handlers.h | 5 +++--
lib/raop_rtp.c | 16 +++++++-------
lib/raop_rtp.h | 2 +-
lib/stream.h | 2 +-
uxplay.cpp | 51 ++++++++++++++++++++++++++++++++++++++-------
6 files changed, 58 insertions(+), 20 deletions(-)
diff --git a/lib/raop.h b/lib/raop.h
index 87f486b..3eb1054 100644
--- a/lib/raop.h
+++ b/lib/raop.h
@@ -84,7 +84,7 @@ struct raop_callbacks_s {
void (*audio_set_coverart)(void *cls, const void *buffer, int buflen);
void (*audio_stop_coverart_rendering) (void* cls);
void (*audio_remote_control_id)(void *cls, const char *dacp_id, const char *active_remote_header);
- void (*audio_set_progress)(void *cls, unsigned int start, unsigned int curr, unsigned int end);
+ void (*audio_set_progress)(void *cls, uint32_t *start, uint32_t *curr, uint32_t *end);
void (*audio_get_format)(void *cls, unsigned char *ct, unsigned short *spf, bool *usingScreen, bool *isMedia, uint64_t *audioFormat);
void (*video_report_size)(void *cls, float *width_source, float *height_source, float *width, float *height);
void (*mirror_video_activity)(void *cls, double *txusage);
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 4d7af19..a3ada61 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -22,6 +22,7 @@
#include "utils.h"
#include
#include
+#include
#include
#define AUDIO_SAMPLE_RATE 44100 /* all supported AirPlay audio format use this sample rate */
#define SECOND_IN_USECS 1000000
@@ -1091,8 +1092,8 @@ raop_handler_set_parameter(raop_conn_t *conn,
sscanf(datastr+8, "%f", &vol);
raop_rtp_set_volume(conn->raop_rtp, vol);
} else if ((datalen >= 10) && !strncmp(datastr, "progress: ", 10)) {
- unsigned int start, curr, end;
- sscanf(datastr+10, "%u/%u/%u", &start, &curr, &end);
+ uint32_t start, curr, end;
+ sscanf(datastr+10, "%"PRIu32"/%"PRIu32"/%"PRIu32, &start, &curr, &end);
raop_rtp_set_progress(conn->raop_rtp, start, curr, end);
}
} else if (!conn->raop_rtp) {
diff --git a/lib/raop_rtp.c b/lib/raop_rtp.c
index e76e90f..d36b47a 100644
--- a/lib/raop_rtp.c
+++ b/lib/raop_rtp.c
@@ -93,9 +93,9 @@ struct raop_rtp_s {
int coverart_len;
char *dacp_id;
char *active_remote_header;
- unsigned int progress_start;
- unsigned int progress_curr;
- unsigned int progress_end;
+ uint32_t progress_start;
+ uint32_t progress_curr;
+ uint32_t progress_end;
int progress_changed;
int flush;
@@ -287,9 +287,9 @@ raop_rtp_process_events(raop_rtp_t *raop_rtp, void *cb_data)
int coverart_len;
char *dacp_id;
char *active_remote_header;
- unsigned int progress_start;
- unsigned int progress_curr;
- unsigned int progress_end;
+ uint32_t progress_start;
+ uint32_t progress_curr;
+ uint32_t progress_end;
int progress_changed;
assert(raop_rtp);
@@ -378,7 +378,7 @@ raop_rtp_process_events(raop_rtp_t *raop_rtp, void *cb_data)
if (progress_changed) {
if (raop_rtp->callbacks.audio_set_progress) {
- raop_rtp->callbacks.audio_set_progress(raop_rtp->callbacks.cls, progress_start, progress_curr, progress_end);
+ raop_rtp->callbacks.audio_set_progress(raop_rtp->callbacks.cls, &progress_start, &progress_curr, &progress_end);
}
}
return 0;
@@ -805,7 +805,7 @@ raop_rtp_remote_control_id(raop_rtp_t *raop_rtp, const char *dacp_id, const char
}
void
-raop_rtp_set_progress(raop_rtp_t *raop_rtp, unsigned int start, unsigned int curr, unsigned int end)
+raop_rtp_set_progress(raop_rtp_t *raop_rtp, uint32_t start, uint32_t curr, uint32_t end)
{
assert(raop_rtp);
diff --git a/lib/raop_rtp.h b/lib/raop_rtp.h
index e1276bd..d9f5476 100644
--- a/lib/raop_rtp.h
+++ b/lib/raop_rtp.h
@@ -39,7 +39,7 @@ void raop_rtp_set_volume(raop_rtp_t *raop_rtp, float volume);
void raop_rtp_set_metadata(raop_rtp_t *raop_rtp, const char *data, int datalen);
void raop_rtp_set_coverart(raop_rtp_t *raop_rtp, const char *data, int datalen);
void raop_rtp_remote_control_id(raop_rtp_t *raop_rtp, const char *dacp_id, const char *active_remote_header);
-void raop_rtp_set_progress(raop_rtp_t *raop_rtp, unsigned int start, unsigned int curr, unsigned int end);
+void raop_rtp_set_progress(raop_rtp_t *raop_rtp, uint32_t start, uint32_t curr, uint32_t end);
void raop_rtp_flush(raop_rtp_t *raop_rtp, int next_seq);
void raop_rtp_stop(raop_rtp_t *raop_rtp);
int raop_rtp_is_running(raop_rtp_t *raop_rtp);
diff --git a/lib/stream.h b/lib/stream.h
index 6905dcd..c544886 100644
--- a/lib/stream.h
+++ b/lib/stream.h
@@ -37,7 +37,7 @@ typedef struct {
int sync_status;
uint64_t ntp_time_local;
uint64_t ntp_time_remote;
- uint64_t rtp_time;
+ uint32_t rtp_time;
unsigned short seqnum;
} audio_decode_struct;
diff --git a/uxplay.cpp b/uxplay.cpp
index 459e3f1..082339c 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -174,12 +174,17 @@ static int n_audio_renderers = 0;
static bool hls_support = false;
static std::string url = "";
static guint gst_x11_window_id = 0;
+static guint progress_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;
static guint playbin_version = DEFAULT_PLAYBIN_VERSION;
static bool reset_httpd = false;
+static bool monitor_progress = false;
+static uint32_t rtptime;
+static uint32_t rtptime_start;
+static uint32_t rtptime_end;
//Support for D-Bus-based screensaver inhibition (org.freedesktop.ScreenSaver)
static unsigned int scrsv;
@@ -558,6 +563,30 @@ static guint g_unix_signal_add(gint signum, GSourceFunc handler, gpointer user_d
}
#endif
+static void display_progress(uint32_t start, uint32_t curr, uint32_t end) {
+ if (curr < start || curr > end) {
+ return;
+ }
+ int duration = (int) (end - start)/44100;
+ int position = (int) (curr - start)/44100;
+ int remain = duration - position;
+ printf("audio progress (min:sec): %3d:%2.2d; remaining: %3d:%2.2d; track length %d:%2.2d\r",
+ position/60, position%60, remain/60, remain%60, duration/60, duration%60);
+ fflush(NULL);
+}
+
+static gboolean progress_callback (gpointer loop) {
+ if (monitor_progress) {
+ if (rtptime_start || rtptime_end) {
+ display_progress(rtptime_start, rtptime, rtptime_end);
+ }
+ return TRUE;
+ } else {
+ progress_id = 0;
+ return FALSE;
+ }
+}
+
#define MAX_VIDEO_RENDERERS 3
#define MAX_AUDIO_RENDERERS 2
static void main_loop() {
@@ -565,6 +594,7 @@ static void main_loop() {
guint gst_audio_bus_watch_id[MAX_AUDIO_RENDERERS] = { 0 };
GMainLoop *loop = g_main_loop_new(NULL,FALSE);
relaunch_video = false;
+ monitor_progress = false;
reset_loop = false;
reset_httpd = false;
preserve_connections = false;
@@ -593,6 +623,10 @@ static void main_loop() {
}
}
if (use_audio) {
+ rtptime_start = 0;
+ rtptime_end = 0;
+ monitor_progress = true;
+ progress_id = g_timeout_add_seconds(1,(GSourceFunc) progress_callback, (gpointer) loop);
n_audio_renderers = 2;
g_assert(n_audio_renderers <= MAX_AUDIO_RENDERERS);
for (int i = 0; i < n_audio_renderers; i++) {
@@ -618,6 +652,7 @@ static void main_loop() {
if (sigint_watch_id > 0) g_source_remove(sigint_watch_id);
if (sigterm_watch_id > 0) g_source_remove(sigterm_watch_id);
if (reset_watch_id > 0) g_source_remove(reset_watch_id);
+ if (progress_id > 0) g_source_remove(progress_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);
@@ -1944,12 +1979,15 @@ extern "C" void audio_process (void *cls, raop_ntp_t *ntp, audio_decode_struct *
data->ntp_time_remote = data->ntp_time_remote + remote_clock_offset;
switch (data->ct) {
case 2:
+ /* for progress monitor (ALAC audio only) */
+ rtptime = data->rtp_time;
if (audio_delay_alac) {
data->ntp_time_remote = (uint64_t) ((int64_t) data->ntp_time_remote + audio_delay_alac);
}
break;
case 4:
case 8:
+ monitor_progress = false;
if (audio_delay_aac) {
data->ntp_time_remote = (uint64_t) ((int64_t) data->ntp_time_remote + audio_delay_aac);
}
@@ -2133,12 +2171,11 @@ extern "C" void audio_stop_coverart_rendering(void *cls) {
}
}
-extern "C" void audio_set_progress(void *cls, unsigned int start, unsigned int curr, unsigned int end) {
- int duration = (int) (end - start)/44100;
- int position = (int) (curr - start)/44100;
- int remain = duration - position;
- printf("audio progress (min:sec): %d:%2.2d; remaining: %d:%2.2d; track length %d:%2.2d\n",
- position/60, position%60, remain/60, remain%60, duration/60, duration%60);
+extern "C" void audio_set_progress(void *cls, uint32_t *start, uint32_t *curr, uint32_t *end) {
+ rtptime_start = *start;
+ rtptime = *curr;
+ rtptime_end = *end;
+ display_progress(rtptime_start, rtptime, rtptime_end);
}
extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
@@ -2147,7 +2184,7 @@ extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
int datalen;
int count = 0;
- printf("==============Audio Metadata=============\n");
+ printf("====================Audio Metadata==================\n");
if (buflen < 8) {
LOGE("received invalid metadata, length %d < 8", buflen);
From 6c2844deef1846ea1da3b9ba9adde704dd1328da Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 31 Aug 2025 11:09:45 -0400
Subject: [PATCH 29/58] raop_buffer.c: cleanup
---
lib/raop_buffer.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/raop_buffer.c b/lib/raop_buffer.c
index 99cd961..29f1f3e 100644
--- a/lib/raop_buffer.c
+++ b/lib/raop_buffer.c
@@ -175,7 +175,7 @@ raop_buffer_enqueue(raop_buffer_t *raop_buffer, unsigned char *data, unsigned sh
return -1;
}
/* before time is synchronized, some empty data packets are sent */
- if (datalen == 16 && !memcmp(&data[12], empty_packet_marker, 4)) {
+ if (datalen == 12 || (datalen == 16 && !memcmp(&data[12], empty_packet_marker, 4))) {
return 0;
}
int payload_size = datalen - 12;
From 6cc0424bc6c5e8072fc1b321e3a7d0b107b010e8 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Mon, 1 Sep 2025 10:37:49 -0400
Subject: [PATCH 30/58] add video_renderer_cycle (for closing expired rendered
coverart)
---
renderers/video_renderer.c | 113 ++++++++++++++++++++++++-------------
renderers/video_renderer.h | 1 +
2 files changed, 76 insertions(+), 38 deletions(-)
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index ca1791f..59a95a6 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -181,7 +181,7 @@ void video_renderer_size(float *f_width_source, float *f_height_source, float *f
}
GstElement *make_video_sink(const char *videosink, const char *videosink_options) {
- /* used to build a videosink for playbin, using the user-specified string "videosink" */
+ /* used to build a videosink for playbin, using the user-specified string "videosink" */
GstElement *video_sink = gst_element_factory_make(videosink, "videosink");
if (!video_sink) {
return NULL;
@@ -222,7 +222,7 @@ GstElement *make_video_sink(const char *videosink, const char *videosink_options
return video_sink;
}
-void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
+void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support, guint playbin_version, const char *uri) {
GError *error = NULL;
@@ -400,9 +400,9 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, vide
renderer_type[i]->gst_window = NULL;
renderer_type[i]->use_x11 = false;
X11_search_attempts = 0;
- /* setting char *x11_display_name to NULL means the value is taken from $DISPLAY in the environment
+ /* setting char *x11_display_name to NULL means the value is taken from $DISPLAY in the environment
* (a uxplay option to specify a different value is possible) */
- char *x11_display_name = NULL;
+ char *x11_display_name = NULL;
if (use_x11) {
if (i == 0) {
renderer_type[0]->gst_window = (X11_Window_t *) calloc(1, sizeof(X11_Window_t));
@@ -469,16 +469,16 @@ void video_renderer_start() {
if (hls_video) {
gst_element_set_state (renderer->pipeline, GST_STATE_PAUSED);
gst_element_get_state(renderer->pipeline, &state, NULL, 1000 * GST_MSECOND);
- state_name= gst_element_state_get_name(state);
+ state_name = gst_element_state_get_name(state);
logger_log(logger, LOGGER_DEBUG, "video renderer_start: state %s", state_name);
return;
}
- /* when not hls, start both h264 and h265 pipelines; will shut down the "wrong" one when we know the codec */
+ /* when not hls, start both h264 and h265 pipelines; will shut down the "wrong" one when we know the codec */
for (int i = 0; i < n_renderers; i++) {
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_PAUSED);
- gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
- state_name= gst_element_state_get_name(state);
- logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d %p state %s", i, renderer_type[i], state_name);
+ gst_element_get_state(renderer_type[i]->pipeline, &state, NULL, 1000 * GST_MSECOND);
+ state_name = gst_element_state_get_name(state);
+ logger_log(logger, LOGGER_DEBUG, "video renderer_start: renderer %d %p state %s", i, renderer_type[i], state_name);
}
renderer = NULL;
first_packet = true;
@@ -507,6 +507,43 @@ bool waiting_for_x11_window() {
return false;
}
+
+/* use this to cycle the jpeg renderer to remove expired coverart when no new coverart has replaced it */
+int video_renderer_cycle() {
+ g_assert(renderer);
+ GstState state, pending_state, target_state;
+ GstStateChangeReturn ret;
+ gst_element_get_state(renderer->pipeline, &state, NULL, 0);
+ logger_log(logger, LOGGER_DEBUG, "renderer_cycle renderer %p: initial pipeline state is %s", renderer,
+ gst_element_state_get_name(state));
+
+ for (int i = 0 ; i < 2; i++) {
+ if (i == 0 ) {
+ target_state = GST_STATE_NULL;
+ video_renderer_stop();
+ } else {
+ target_state = GST_STATE_PLAYING;
+ gst_element_set_state (renderer->pipeline, target_state);
+ }
+ while (state != target_state) {
+ ret = gst_element_get_state(renderer->pipeline, &state, &pending_state, 100 * GST_MSECOND);
+ if (ret == GST_STATE_CHANGE_SUCCESS) {
+ logger_log(logger, LOGGER_DEBUG, "current pipeline state is %s", gst_element_state_get_name(state));
+ if (pending_state != GST_STATE_VOID_PENDING) {
+ logger_log(logger, LOGGER_DEBUG, "pending pipeline state is %s", gst_element_state_get_name(pending_state));
+ }
+ } else if (ret == GST_STATE_CHANGE_FAILURE) {
+ logger_log(logger, LOGGER_ERR, "pipeline %s: state change to %s failed", renderer->codec, gst_element_state_get_name(target_state));
+ return -1;
+ } else if (ret == GST_STATE_CHANGE_ASYNC) {
+ logger_log(logger, LOGGER_DEBUG, "state change to %s is asynchronous, waiting for completion ...",
+ gst_element_state_get_name(target_state));
+ }
+ }
+ }
+ return 0;
+}
+
void video_renderer_display_jpeg(const void *data, int *data_len) {
GstBuffer *buffer;
if (type_jpeg == -1) {
@@ -633,32 +670,32 @@ void video_renderer_destroy() {
}
static void get_stream_status_name(GstStreamStatusType type, char *name, size_t len) {
- switch (type) {
- case GST_STREAM_STATUS_TYPE_CREATE:
- strncpy(name, "CREATE", len);
- return;
- case GST_STREAM_STATUS_TYPE_ENTER:
- strncpy(name, "ENTER", len);
- return;
- case GST_STREAM_STATUS_TYPE_LEAVE:
- strncpy(name, "LEAVE", len);
- return;
- case GST_STREAM_STATUS_TYPE_DESTROY:
- strncpy(name, "DESTROY", len);
- return;
- case GST_STREAM_STATUS_TYPE_START:
- strncpy(name, "START", len);
- return;
- case GST_STREAM_STATUS_TYPE_PAUSE:
- strncpy(name, "PAUSE", len);
- return;
- case GST_STREAM_STATUS_TYPE_STOP:
- strncpy(name, "STOP", len);
- return;
- default:
- strncpy(name, "", len);
- return;
- }
+ switch (type) {
+ case GST_STREAM_STATUS_TYPE_CREATE:
+ strncpy(name, "CREATE", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_ENTER:
+ strncpy(name, "ENTER", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_LEAVE:
+ strncpy(name, "LEAVE", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_DESTROY:
+ strncpy(name, "DESTROY", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_START:
+ strncpy(name, "START", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_PAUSE:
+ strncpy(name, "PAUSE", len);
+ return;
+ case GST_STREAM_STATUS_TYPE_STOP:
+ strncpy(name, "STOP", len);
+ return;
+ default:
+ strncpy(name, "", len);
+ return;
+ }
}
static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *message, void *loop) {
@@ -692,7 +729,7 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
if (logger_debug) {
gchar *name = NULL;
- GstElement *element = NULL;
+ GstElement *element = NULL;
gchar type_name[8] = { 0 };
if (GST_MESSAGE_TYPE(message) == GST_MESSAGE_STREAM_STATUS) {
GstStreamStatusType type;
@@ -793,9 +830,9 @@ static gboolean gstreamer_video_pipeline_bus_callback(GstBus *bus, GstMessage *m
break;
}
case GST_MESSAGE_EOS:
- /* end-of-stream */
+ /* end-of-stream */
logger_log(logger, LOGGER_INFO, "GStreamer: End-Of-Stream (video)");
- if (hls_video) {
+ if (hls_video) {
gst_bus_set_flushing(bus, TRUE);
gst_element_set_state (renderer_type[type]->pipeline, GST_STATE_READY);
renderer_type[type]->terminate = TRUE;
diff --git a/renderers/video_renderer.h b/renderers/video_renderer.h
index 5a0084f..8e96496 100644
--- a/renderers/video_renderer.h
+++ b/renderers/video_renderer.h
@@ -57,6 +57,7 @@ void video_renderer_pause ();
void video_renderer_seek(float position);
void video_renderer_set_start(float position);
void video_renderer_resume ();
+int video_renderer_cycle ();
bool video_renderer_is_paused();
uint64_t video_renderer_render_buffer (unsigned char* data, int *data_len, int *nal_count, uint64_t *ntp_time);
void video_renderer_display_jpeg(const void *data, int *data_len);
From ae8e427fe34b3911563fad453c1c80a99fdd9707 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 2 Sep 2025 01:28:51 -0400
Subject: [PATCH 31/58] stop rendering of expired coverart when there is no
replacement
---
renderers/video_renderer.c | 13 +++++++++----
uxplay.cpp | 30 ++++++++++++++++++++++++++----
2 files changed, 35 insertions(+), 8 deletions(-)
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 59a95a6..3fd4550 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -507,10 +507,11 @@ bool waiting_for_x11_window() {
return false;
}
-
/* use this to cycle the jpeg renderer to remove expired coverart when no new coverart has replaced it */
int video_renderer_cycle() {
- g_assert(renderer);
+ if (!renderer || !strstr(renderer->codec, jpeg)) {
+ return -1;
+ }
GstState state, pending_state, target_state;
GstStateChangeReturn ret;
gst_element_get_state(renderer->pipeline, &state, NULL, 0);
@@ -518,6 +519,7 @@ int video_renderer_cycle() {
gst_element_state_get_name(state));
for (int i = 0 ; i < 2; i++) {
+ int count = 0;
if (i == 0 ) {
target_state = GST_STATE_NULL;
video_renderer_stop();
@@ -526,7 +528,7 @@ int video_renderer_cycle() {
gst_element_set_state (renderer->pipeline, target_state);
}
while (state != target_state) {
- ret = gst_element_get_state(renderer->pipeline, &state, &pending_state, 100 * GST_MSECOND);
+ ret = gst_element_get_state(renderer->pipeline, &state, &pending_state, 1000 * GST_MSECOND);
if (ret == GST_STATE_CHANGE_SUCCESS) {
logger_log(logger, LOGGER_DEBUG, "current pipeline state is %s", gst_element_state_get_name(state));
if (pending_state != GST_STATE_VOID_PENDING) {
@@ -534,7 +536,10 @@ int video_renderer_cycle() {
}
} else if (ret == GST_STATE_CHANGE_FAILURE) {
logger_log(logger, LOGGER_ERR, "pipeline %s: state change to %s failed", renderer->codec, gst_element_state_get_name(target_state));
- return -1;
+ count++;
+ if (count > 10) {
+ return -1;
+ }
} else if (ret == GST_STATE_CHANGE_ASYNC) {
logger_log(logger, LOGGER_DEBUG, "state change to %s is asynchronous, waiting for completion ...",
gst_element_state_get_name(target_state));
diff --git a/uxplay.cpp b/uxplay.cpp
index 082339c..f8fbe62 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -182,9 +182,12 @@ static guint missed_feedback = 0;
static guint playbin_version = DEFAULT_PLAYBIN_VERSION;
static bool reset_httpd = false;
static bool monitor_progress = false;
-static uint32_t rtptime;
-static uint32_t rtptime_start;
-static uint32_t rtptime_end;
+static uint32_t rtptime = 0;
+static uint32_t rtptime_start = 0;
+static uint32_t rtptime_end = 0;
+static uint32_t rtptime_coverart_expired = 0;
+static std::string artist;
+static std::string coverart_artist;
//Support for D-Bus-based screensaver inhibition (org.freedesktop.ScreenSaver)
static unsigned int scrsv;
@@ -580,6 +583,11 @@ static gboolean progress_callback (gpointer loop) {
if (rtptime_start || rtptime_end) {
display_progress(rtptime_start, rtptime, rtptime_end);
}
+ if (render_coverart && coverart_artist == "_expired_" && rtptime - rtptime_coverart_expired > 44100 * 5) {
+ /* remove any expired coverart still being rendered more than 5 secs after it expired */
+ coverart_artist.erase();
+ video_renderer_cycle();
+ }
return TRUE;
} else {
progress_id = 0;
@@ -626,6 +634,8 @@ static void main_loop() {
rtptime_start = 0;
rtptime_end = 0;
monitor_progress = true;
+ artist.erase();
+ coverart_artist.erase();
progress_id = g_timeout_add_seconds(1,(GSourceFunc) progress_callback, (gpointer) loop);
n_audio_renderers = 2;
g_assert(n_audio_renderers <= MAX_AUDIO_RENDERERS);
@@ -1541,6 +1551,17 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
break;
case 'r':
metadata_text->append("Artist: "); /*asar*/
+ if (render_coverart) {
+ artist.erase();
+ artist.append(metadata, metadata + datalen);
+ if (coverart_artist == "_pending_") {
+ coverart_artist = artist;
+ }
+ if (coverart_artist != "_expired_" && coverart_artist != artist) {
+ coverart_artist = "_expired_";
+ rtptime_coverart_expired = rtptime;
+ }
+ }
break;
default:
dmap_type = 0;
@@ -2161,7 +2182,8 @@ extern "C" void audio_set_coverart(void *cls, const void *buffer, int buflen) {
LOGI("coverart size %d written to %s", buflen, coverart_filename.c_str());
} else if (buffer && render_coverart) {
video_renderer_choose_codec(true, false); /* video_is_jpeg = true */
- video_renderer_display_jpeg(buffer, &buflen);
+ video_renderer_display_jpeg(buffer, &buflen);
+ coverart_artist = "_pending_";
}
}
From 117febfe283200b06e274ab2092305f79439b8e6 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 2 Sep 2025 11:42:33 -0400
Subject: [PATCH 32/58] missing break in switch code (issue seen in AirMyPC
debug output)
---
lib/raop_rtp_mirror.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c
index e2a00f0..1ca548d 100644
--- a/lib/raop_rtp_mirror.c
+++ b/lib/raop_rtp_mirror.c
@@ -788,6 +788,7 @@ raop_rtp_mirror_thread(void *arg)
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived old-protocol once-per-second packet from client:"
" payload_size %d header %s ts_raw = %llu", payload_size, packet_description, ntp_timestamp_raw);
/* "old protocol" (used by AirMyPC), rest of 128-byte packet is empty */
+ break;
case 0x05:
logger_log(raop_rtp_mirror->logger, LOGGER_DEBUG, "\nReceived video streaming performance info packet from client:"
" payload_size %d header %s ts_raw = %llu", payload_size, packet_description, ntp_timestamp_raw);
From e38a2bfc5bd69deb2dcba9dc4aac5c03b0208015 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 2 Sep 2025 12:53:54 -0400
Subject: [PATCH 33/58] correct feedback error message (signal expected every 2
secs)
---
uxplay.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/uxplay.cpp b/uxplay.cpp
index f8fbe62..d956843 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -507,7 +507,7 @@ static gboolean feedback_callback(gpointer loop) {
g_main_loop_quit((GMainLoop *) loop);
return TRUE;
} else if (missed_feedback > 2) {
- LOGE("%u missed client feedback signals (expected once per second); client may be offline", missed_feedback);
+ LOGE("%u missed client feedback signals (expected every two seconds); client may be offline", missed_feedback);
}
missed_feedback++;
} else {
From 51829e585c4de6a8c8b6df8a14c6e6f453915161 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Wed, 3 Sep 2025 18:46:21 -0400
Subject: [PATCH 34/58] fix typo in force-modesetting
---
uxplay.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/uxplay.cpp b/uxplay.cpp
index d956843..6b32108 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -2681,13 +2681,13 @@ int main (int argc, char *argv[]) {
if (videosink == "waylandsink" || videosink == "vaapisink") {
videosink_options.append(" fullscreen=true");
} else if (videosink == "kmssink") {
- videosink_options.append(" force_modesetting=true");
+ videosink_options.append(" force-modesetting=TRUE ");
}
}
if (videosink == "d3d11videosink" && videosink_options.empty() && use_video) {
if (fullscreen) {
- videosink_options.append(" fullscreen-toggle-mode=GST_D3D11_WINDOW_FULLSCREEN_TOGGLE_MODE_PROPERTY fullscreen=TRUE");
+ videosink_options.append(" fullscreen-toggle-mode=GST_D3D11_WINDOW_FULLSCREEN_TOGGLE_MODE_PROPERTY fullscreen=TRUE ");
} else {
videosink_options.append(" fullscreen-toggle-mode=GST_D3D11_WINDOW_FULLSCREEN_TOGGLE_MODE_ALT_ENTER ");
LOGI("Use Alt-Enter key combination to toggle into/out of full-screen mode");
@@ -2696,7 +2696,7 @@ int main (int argc, char *argv[]) {
if (videosink == "d3d12videosink" && videosink_options.empty() && use_video) {
if (fullscreen) {
- videosink_options.append("fullscreen=TRUE");
+ videosink_options.append(" fullscreen=TRUE ");
} else {
videosink_options.append(" fullscreen-on-alt-enter=TRUE ");
LOGI("Use Alt-Enter key combination to toggle into/out of full-screen mode");
From db8b6a96c3f2e655cef0044e0e8db2050eb444fc Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Wed, 3 Sep 2025 22:30:29 -0400
Subject: [PATCH 35/58] terminate if video renderer fails to initialize
---
renderers/video_renderer.c | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 3fd4550..9cea88e 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -425,17 +425,19 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, video
renderer_type[i]->bus = gst_element_get_bus(renderer_type[i]->pipeline);
gst_element_set_state (renderer_type[i]->pipeline, GST_STATE_READY);
GstState state;
- if (gst_element_get_state (renderer_type[i]->pipeline, &state, NULL, 100 * GST_MSECOND)) {
- if (state == GST_STATE_READY) {
- logger_log(logger, LOGGER_DEBUG, "Initialized GStreamer video renderer %d", i + 1);
- if (hls_video && i == 0) {
- renderer = renderer_type[i];
- }
- } else {
- logger_log(logger, LOGGER_ERR, "Failed to initialize GStreamer video renderer %d", i + 1);
+ GstStateChangeReturn ret = gst_element_get_state (renderer_type[i]->pipeline, &state, NULL, 100 * GST_MSECOND);
+ if (ret == GST_STATE_CHANGE_SUCCESS) {
+ logger_log(logger, LOGGER_DEBUG, "Initialized GStreamer video renderer %d", i + 1);
+ if (hls_video && i == 0) {
+ renderer = renderer_type[i];
}
} else {
logger_log(logger, LOGGER_ERR, "Failed to initialize GStreamer video renderer %d", i + 1);
+ logger_log(logger, LOGGER_INFO, "\nPerhaps your GStreamer installation is missing some required plugins,"
+ "\nor your choices of video options (-vs -vd -vc -fs etc.) are incompatible on"
+ "\nthis computer architecture. (An example: kmssink with fullscreen option -fs"
+ "\nmay work on some systems, but fail on others)");
+ exit(1);
}
}
}
From fac526735c9c5955d38eee6218315969066d42a2 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 7 Sep 2025 00:26:17 -0400
Subject: [PATCH 36/58] Simplify response to first GET/Info with AirPlay/RAOP
qualifier
---
lib/dnssd.c | 7 ++
lib/dnssd.h | 1 +
lib/raop_handlers.h | 161 +++++++++++++++++++++++++++-----------------
3 files changed, 109 insertions(+), 60 deletions(-)
diff --git a/lib/dnssd.c b/lib/dnssd.c
index 23d3143..44adac2 100644
--- a/lib/dnssd.c
+++ b/lib/dnssd.c
@@ -412,6 +412,13 @@ dnssd_register_airplay(dnssd_t *dnssd, unsigned short port)
return (int) retval; /* error codes are listed in Apple's dns_sd.h */
}
+const char *
+dnssd_get_raop_txt(dnssd_t *dnssd, int *length)
+{
+ *length = dnssd->TXTRecordGetLength(&dnssd->raop_record);
+ return dnssd->TXTRecordGetBytesPtr(&dnssd->raop_record);
+}
+
const char *
dnssd_get_airplay_txt(dnssd_t *dnssd, int *length)
{
diff --git a/lib/dnssd.h b/lib/dnssd.h
index 968d523..007a83a 100644
--- a/lib/dnssd.h
+++ b/lib/dnssd.h
@@ -43,6 +43,7 @@ DNSSD_API int dnssd_register_airplay(dnssd_t *dnssd, unsigned short port);
DNSSD_API void dnssd_unregister_raop(dnssd_t *dnssd);
DNSSD_API void dnssd_unregister_airplay(dnssd_t *dnssd);
+DNSSD_API const char *dnssd_get_raop_txt(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_airplay_txt(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_name(dnssd_t *dnssd, int *length);
DNSSD_API const char *dnssd_get_hw_addr(dnssd_t *dnssd, int *length);
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index a3ada61..1e1bd8b 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -32,6 +32,14 @@
typedef void (*raop_handler_t)(raop_conn_t *, http_request_t *,
http_response_t *, char **, int *);
+#ifndef PLIST_230
+void plist_mem_free(void* ptr) {
+ if (ptr) {
+ free(ptr);
+ }
+}
+#endif
+
static void
raop_handler_info(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
@@ -39,11 +47,27 @@ raop_handler_info(raop_conn_t *conn,
{
assert(conn->raop->dnssd);
-#if 0
- /* initial GET/info request sends plist with string "txtAirPlay" */
- bool txtAirPlay = false;
+ /* There are three possible RTSP/1.0 GET/info requests
+ (1) with a CSeq number and with a plist
+ (2) with a CSeq number and without a plist
+ (3) without a CSeq number or a Header (part of Bluetooth LE Service Discovery) */
+
+ const char* url = NULL;
const char* content_type = NULL;
+ const char* cseq = NULL;
+ url = http_request_get_url(request);
content_type = http_request_get_header(request, "Content-Type");
+ cseq = http_request_get_header(request, "CSeq");
+ int len;
+ bool add_txt_airplay = false;
+ bool add_txt_raop = false;
+ const char txtRAOP[] = "txtRAOP";
+ const char txtAirPlay[] = "txtAirPlay";
+
+
+ plist_t res_node = plist_new_dict();
+
+ /* initial GET/info request sends plist with string "txtAirPlay" */
if (content_type && strstr(content_type, "application/x-apple-binary-plist")) {
char *qualifier_string = NULL;
const char *data = NULL;
@@ -57,17 +81,40 @@ raop_handler_info(raop_conn_t *conn,
plist_t req_string_node = plist_array_get_item(req_qualifier_node, 0);
plist_get_string_val(req_string_node, &qualifier_string);
}
- if (qualifier_string && !strcmp(qualifier_string, "txtAirPlay")) {
- printf("qualifier: %s\n", qualifier_string);
- txtAirPlay = true;
- }
if (qualifier_string) {
- free(qualifier_string);
+ if (!strcmp(qualifier_string, txtAirPlay )) {
+ add_txt_airplay = true;
+ } else if (!strcmp(qualifier_string, txtRAOP)) {
+ add_txt_raop = true;
+ }
+ plist_mem_free(qualifier_string);
}
}
-#endif
- plist_t res_node = plist_new_dict();
+
+ /* Bluetoth LE discovery protocol request */
+ if (!cseq) {
+ add_txt_airplay = (bool) strstr(url, txtAirPlay);
+ add_txt_raop = (bool) strstr(url, txtRAOP);
+ }
+
+ if (add_txt_airplay) {
+ const char *txt = dnssd_get_airplay_txt(conn->raop->dnssd, &len);
+ plist_t txt_airplay_node = plist_new_data(txt, len);
+ plist_dict_set_item(res_node, txtAirPlay, txt_airplay_node);
+ }
+
+ if (add_txt_raop) {
+ const char *txt = dnssd_get_raop_txt(conn->raop->dnssd, &len);
+ plist_t txt_raop_node = plist_new_data(txt, len);
+ plist_dict_set_item(res_node, txtRAOP, txt_raop_node);
+ }
+
+ /* don't need anything below here in the response to initial "txtAirPlay" GET/info request */
+ if (content_type) {
+ goto finished;
+ }
+
/* deviceID is the physical hardware address, and will not change */
int hw_addr_raw_len = 0;
const char *hw_addr_raw = dnssd_get_hw_addr(conn->raop->dnssd, &hw_addr_raw_len);
@@ -77,17 +124,16 @@ raop_handler_info(raop_conn_t *conn,
plist_t device_id_node = plist_new_string(hw_addr);
plist_dict_set_item(res_node, "deviceID", device_id_node);
+ plist_t mac_address_node = plist_new_string(hw_addr);
+ plist_dict_set_item(res_node, "macAddress", mac_address_node);
+ free(hw_addr);
+
/* Persistent Public Key */
int pk_len = 0;
char *pk = utils_parse_hex(conn->raop->pk_str, strlen(conn->raop->pk_str), &pk_len);
plist_t pk_node = plist_new_data(pk, pk_len);
plist_dict_set_item(res_node, "pk", pk_node);
-
- /* airplay_txt is from the _airplay._tcp dnssd announuncement, may not be necessary */
- int airplay_txt_len = 0;
- const char *airplay_txt = dnssd_get_airplay_txt(conn->raop->dnssd, &airplay_txt_len);
- plist_t txt_airplay_node = plist_new_data(airplay_txt, airplay_txt_len);
- plist_dict_set_item(res_node, "txtAirPlay", txt_airplay_node);
+ free(pk);
uint64_t features = dnssd_get_airplay_features(conn->raop->dnssd);
plist_t features_node = plist_new_uint(features);
@@ -98,6 +144,32 @@ raop_handler_info(raop_conn_t *conn,
plist_t name_node = plist_new_string(name);
plist_dict_set_item(res_node, "name", name_node);
+ plist_t pi_node = plist_new_string(AIRPLAY_PI);
+ plist_dict_set_item(res_node, "pi", pi_node);
+
+ plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10));
+ plist_dict_set_item(res_node, "vv", vv_node);
+
+ plist_t status_flags_node = plist_new_uint(68);
+ plist_dict_set_item(res_node, "statusFlags", status_flags_node);
+
+ plist_t keep_alive_low_power_node = plist_new_uint(1);
+ plist_dict_set_item(res_node, "keepAliveLowPower", keep_alive_low_power_node);
+
+ plist_t source_version_node = plist_new_string(GLOBAL_VERSION);
+ plist_dict_set_item(res_node, "sourceVersion", source_version_node);
+
+ plist_t keep_alive_send_stats_as_body_node = plist_new_bool(1);
+ plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
+
+ plist_t model_node = plist_new_string(GLOBAL_MODEL);
+ plist_dict_set_item(res_node, "model", model_node);
+
+ /* dont need anything below here in the Bluetooth LE response */
+ if (cseq == NULL) {
+ goto finished;
+ }
+
plist_t audio_latencies_node = plist_new_array();
plist_t audio_latencies_0_node = plist_new_dict();
plist_t audio_latencies_0_output_latency_micros_node = plist_new_bool(0);
@@ -142,30 +214,6 @@ raop_handler_info(raop_conn_t *conn,
plist_array_append_item(audio_formats_node, audio_format_1_node);
plist_dict_set_item(res_node, "audioFormats", audio_formats_node);
- plist_t pi_node = plist_new_string(AIRPLAY_PI);
- plist_dict_set_item(res_node, "pi", pi_node);
-
- plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10));
- plist_dict_set_item(res_node, "vv", vv_node);
-
- plist_t status_flags_node = plist_new_uint(68);
- plist_dict_set_item(res_node, "statusFlags", status_flags_node);
-
- plist_t keep_alive_low_power_node = plist_new_uint(1);
- plist_dict_set_item(res_node, "keepAliveLowPower", keep_alive_low_power_node);
-
- plist_t source_version_node = plist_new_string(GLOBAL_VERSION);
- plist_dict_set_item(res_node, "sourceVersion", source_version_node);
-
- plist_t keep_alive_send_stats_as_body_node = plist_new_bool(1);
- plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node);
-
- plist_t model_node = plist_new_string(GLOBAL_MODEL);
- plist_dict_set_item(res_node, "model", model_node);
-
- plist_t mac_address_node = plist_new_string(hw_addr);
- plist_dict_set_item(res_node, "macAddress", mac_address_node);
-
plist_t displays_node = plist_new_array();
plist_t displays_0_node = plist_new_dict();
plist_t displays_0_width_physical_node = plist_new_uint(0);
@@ -196,11 +244,10 @@ raop_handler_info(raop_conn_t *conn,
plist_array_append_item(displays_node, displays_0_node);
plist_dict_set_item(res_node, "displays", displays_node);
+ finished:
plist_to_bin(res_node, response_data, (uint32_t *) response_datalen);
plist_free(res_node);
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
- free(pk);
- free(hw_addr);
}
static void
@@ -280,7 +327,8 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
plist_free (req_root_node);
return;
}
- free (method);
+ plist_mem_free(method);
+ method = NULL;
plist_get_string_val(req_user_node, &user);
logger_log(conn->raop->logger, LOGGER_INFO, "pair-setup-pin: device_id = %s", user);
snprintf(pin, 6, "%04u", conn->raop->pin % 10000);
@@ -289,7 +337,8 @@ raop_handler_pairsetup_pin(raop_conn_t *conn,
}
int ret = srp_new_user(conn->session, conn->raop->pairing, (const char *) user,
(const char *) pin, &salt, &len_salt, &pk, &len_pk);
- free(user);
+ plist_mem_free(user);
+ user = NULL;
plist_free(req_root_node);
if (ret < 0) {
logger_log(conn->raop->logger, LOGGER_ERR, "failed to create user, err = %d", ret);
@@ -711,18 +760,12 @@ raop_handler_setup(raop_conn_t *conn,
free (client_pk);
}
}
- if (deviceID) {
- free (deviceID);
- deviceID = NULL;
- }
- if (model) {
- free (model);
- model = NULL;
- }
- if (name) {
- free (name);
- name = NULL;
- }
+ plist_mem_free(deviceID);
+ deviceID = NULL;
+ plist_mem_free(model);
+ model = NULL;
+ plist_mem_free(name);
+ name = NULL;
if (admit_client == false) {
/* client is not authorized to connect */
plist_free(res_root_node);
@@ -834,7 +877,7 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "Client specified timingProtocol=%s,"
" but timingProtocol= NTP is required here", timing_protocol);
}
- free (timing_protocol);
+ plist_mem_free (timing_protocol);
timing_protocol = NULL;
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "Client did not specify timingProtocol,"
@@ -1133,9 +1176,7 @@ raop_handler_audiomode(raop_conn_t *conn,
/* not sure what should be done with this request: usually audioMode requested is "default" */
int log_level = (strstr(audiomode, "default") ? LOGGER_DEBUG : LOGGER_INFO);
logger_log(conn->raop->logger, log_level, "Unhandled RTSP request \"audioMode: %s\"", audiomode);
- if (audiomode) {
- free(audiomode);
- }
+ plist_mem_free(audiomode);
plist_free(req_root_node);
}
From 830c7131feae076711bebd1c874d6c120a18b7ca Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 14 Sep 2025 04:22:46 -0400
Subject: [PATCH 37/58] cleanup plist_get_string_val items (free string)
---
lib/http_handlers.h | 27 ++++++++++++++++-----------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/lib/http_handlers.h b/lib/http_handlers.h
index 02ebcba..93c65c9 100644
--- a/lib/http_handlers.h
+++ b/lib/http_handlers.h
@@ -411,9 +411,13 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
char *type = NULL;
plist_get_string_val(req_type_node, &type);
logger_log(conn->raop->logger, LOGGER_DEBUG, "action type is %s", type);
- if (strstr(type, "unhandledURLResponse")) {
+ bool unhandled_url_response = strstr(type, "unhandledURLResponse");
+ bool playlist_remove = strstr(type, "playlistRemove");
+ bool playlist_insert = strstr(type, "playlistInsert");
+ plist_mem_free(type);
+ if (unhandled_url_response) {
goto unhandledURLResponse;
- } else if (strstr(type, "playlistRemove")) {
+ } else if (playlist_remove) {
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistRemove (stop playback)");
req_params_node = plist_dict_get_item(req_root_node, "params");
if (!req_params_node || !PLIST_IS_DICT (req_params_node)) {
@@ -436,11 +440,11 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "removal_uuid matches playback_uuid\n");
}
- free (remove_uuid);
+ plist_mem_free (remove_uuid);
}
logger_log(conn->raop->logger, LOGGER_ERR, "FIXME: playlist removal not yet implemented");
goto finish;
- } else if (strstr(type, "playlistInsert")) {
+ } else if (playlist_insert) {
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistInsert (add new playback)");
printf("\n***************FIXME************************\nPlaylist insertion needs more information for it to be implemented:\n"
"please report following output as an \"Issue\" at http://github.com/FDH2/UxPlay:\n");
@@ -518,12 +522,14 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
char *fcup_response_url = NULL;
plist_get_string_val(req_params_fcup_response_url_node, &fcup_response_url);
if (!fcup_response_url) {
+ plist_mem_free(fcup_response_url);
goto post_action_error;
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response_URL = %s", fcup_response_url);
plist_t req_params_fcup_response_data_node = plist_dict_get_item(req_params_node, "FCUP_Response_Data");
if (!PLIST_IS_DATA(req_params_fcup_response_data_node)){
+ plist_mem_free(fcup_response_url);
goto post_action_error;
}
@@ -534,6 +540,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (!fcup_response_data) {
free (fcup_response_url);
+ plist_mem_free(fcup_response_url);
goto post_action_error;
}
@@ -581,9 +588,7 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
if (fcup_response_data) {
free (fcup_response_data);
}
- if (fcup_response_url) {
- free (fcup_response_url);
- }
+ plist_mem_free(fcup_response_url);
int num_uri = get_num_media_uri(conn->raop->airplay_video);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
@@ -680,7 +685,7 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
char* playback_uuid = NULL;
plist_get_string_val(req_uuid_node, &playback_uuid);
set_playback_uuid(conn->raop->airplay_video, playback_uuid);
- free (playback_uuid);
+ plist_mem_free (playback_uuid);
}
plist_t req_content_location_node = plist_dict_get_item(req_root_node, "Content-Location");
@@ -699,6 +704,7 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
logger_log(conn->raop->logger, LOGGER_WARNING, "Unsupported HLS streaming format: clientProcName %s not found in supported list: %s",
client_proc_name, supported_hls_proc_names);
}
+ plist_mem_free(client_proc_name);
}
plist_t req_start_position_seconds_node = plist_dict_get_item(req_root_node, "Start-Position-Seconds");
@@ -722,9 +728,7 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
set_next_media_uri_id(conn->raop->airplay_video, 0);
fcup_request((void *) conn, playback_location, apple_session_id, get_next_FCUP_RequestID(conn->raop->airplay_video));
- if (playback_location) {
- free (playback_location);
- }
+ plist_mem_free(playback_location);
if (req_root_node) {
plist_free(req_root_node);
@@ -732,6 +736,7 @@ http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *r
return;
play_error:;
+ plist_mem_free(playback_location);
if (req_root_node) {
plist_free(req_root_node);
}
From e5d8a02952684d2f5ff0f996bc8c4e9c7674def5 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 14 Sep 2025 04:38:39 -0400
Subject: [PATCH 38/58] add port information when accepting clients
---
lib/httpd.c | 9 +++++----
lib/netutils.c | 8 +++++++-
lib/netutils.h | 2 +-
3 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/lib/httpd.c b/lib/httpd.c
index 97b1cbd..db141ce 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -268,6 +268,7 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
struct sockaddr_storage local_saddr;
socklen_t local_saddrlen;
unsigned char *local, *remote;
+ unsigned short port;
unsigned int local_zone_id, remote_zone_id;
int local_len, remote_len;
int ret, fd;
@@ -287,10 +288,10 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
return 0;
}
- logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d",
- (is_ipv6 ? "IPv6" : "IPv4"), fd);
- local = netutils_get_address(&local_saddr, &local_len, &local_zone_id);
- remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id);
+ local = netutils_get_address(&local_saddr, &local_len, &local_zone_id, &port);
+ logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d, port %u",
+ (is_ipv6 ? "IPv6" : "IPv4"), fd, port);
+ remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id, NULL);
assert (local_zone_id == remote_zone_id);
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len, local_zone_id);
diff --git a/lib/netutils.c b/lib/netutils.c
index 642efba..ed68530 100644
--- a/lib/netutils.c
+++ b/lib/netutils.c
@@ -53,7 +53,7 @@ netutils_cleanup()
}
unsigned char *
-netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id)
+netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id, unsigned short *port)
{
unsigned char ipv4_prefix[] = { 0,0,0,0,0,0,0,0,0,0,255,255 };
struct sockaddr *address = sockaddr;
@@ -66,11 +66,17 @@ netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id)
*zone_id = 0;
sin = (struct sockaddr_in *)address;
*length = sizeof(sin->sin_addr.s_addr);
+ if (port) {
+ *port = ntohs(sin->sin_port);
+ }
return (unsigned char *)&sin->sin_addr.s_addr;
} else if (address->sa_family == AF_INET6) {
struct sockaddr_in6 *sin6;
sin6 = (struct sockaddr_in6 *)address;
+ if (port) {
+ *port = ntohs(sin6->sin6_port);
+ }
if (!memcmp(sin6->sin6_addr.s6_addr, ipv4_prefix, 12)) {
/* Actually an embedded IPv4 address */
*zone_id = 0;
diff --git a/lib/netutils.h b/lib/netutils.h
index e64c638..d6b7542 100644
--- a/lib/netutils.h
+++ b/lib/netutils.h
@@ -19,7 +19,7 @@ int netutils_init();
void netutils_cleanup();
int netutils_init_socket(unsigned short *port, int use_ipv6, int use_udp);
-unsigned char *netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id);
+unsigned char *netutils_get_address(void *sockaddr, int *length, unsigned int *zone_id, unsigned short *port);
int netutils_parse_address(int family, const char *src, void *dst, int dstlen);
#endif
From ae52b2bd3a112f28c8797d29fa87aa5bb5b2997a Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 14 Sep 2025 10:54:24 -0400
Subject: [PATCH 39/58] clean up random_mac()
---
uxplay.cpp | 24 ++++++++++--------------
1 file changed, 10 insertions(+), 14 deletions(-)
diff --git a/uxplay.cpp b/uxplay.cpp
index 6b32108..3b73e01 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -63,6 +63,7 @@
#include "lib/stream.h"
#include "lib/logger.h"
#include "lib/dnssd.h"
+#include "lib/crypto.h"
#include "renderers/video_renderer.h"
#include "renderers/audio_renderer.h"
#ifdef DBUS
@@ -784,10 +785,6 @@ static std::string find_mac () {
return mac;
}
-#define MULTICAST 0
-#define LOCAL 1
-#define OCTETS 6
-
static bool validate_mac(char * mac_address) {
char c;
if (strlen(mac_address) != 17) return false;
@@ -806,16 +803,16 @@ static bool validate_mac(char * mac_address) {
}
static std::string random_mac () {
- char str[3];
- int octet = rand() % 64;
- octet = (octet << 1) + LOCAL;
- octet = (octet << 1) + MULTICAST;
- snprintf(str,3,"%02x",octet);
+ char str[4];
+ unsigned char random[6];
+ get_random_bytes(random, sizeof(random));
+ /* mark MAC address as locally administered, i.e. random */
+ random[0] = random[0] & ~0x01;
+ random[0] = random[0] | 0x02;
+ snprintf(str,3,"%2.2x", random[0]);
std::string mac_address(str);
- for (int i = 1; i < OCTETS; i++) {
- mac_address = mac_address + ":";
- octet = rand() % 256;
- snprintf(str,3,"%02x",octet);
+ for (int i = 1; i < 6; i++) {
+ snprintf(str,4,":%2.2x", random[i]);
mac_address = mac_address + str;
}
return mac_address;
@@ -2806,7 +2803,6 @@ int main (int argc, char *argv[]) {
}
}
if (mac_address.empty()) {
- srand(time(NULL) * getpid());
mac_address = random_mac();
LOGI("using randomly-generated MAC address %s",mac_address.c_str());
}
From 469f385502664a0069ad39d55c24287383ea27ed Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Mon, 15 Sep 2025 14:19:05 -0400
Subject: [PATCH 40/58] Add support for bluetooth LE beacon service discovery
based on remarkable work by @connorh315 on getting this working
---
README.html | 237 ++++++++++++++++++++++++++++++++++++++++++++++-
README.md | 205 +++++++++++++++++++++++++++++++++++++++-
README.txt | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++--
lib/raop.c | 23 +++--
uxplay.1 | 2 +
uxplay.cpp | 94 +++++++++++++++----
6 files changed, 782 insertions(+), 41 deletions(-)
diff --git a/README.html b/README.html
index e40a0b3..e9eb958 100644
--- a/README.html
+++ b/README.html
@@ -8,6 +8,19 @@ developed at the GitHub site https://github.com/FDH2/UxPlay (where ALL user issues
should be posted, and latest versions can be found).
+NEW on github: Support for service
+discovery using a Bluetooth LE “beacon” (as an alternative to
+Bonjour/Rendezvous DNS-SD service discovery). The user must set up a
+Bluetooth LE “beacon”, (a USB 4.0 or later “dongle” can be used). See
+instructions below. The beacon runs independently of UxPlay and
+regularly broadcasts a Bluetooth LE (“Low Energy”) 44 byte packet
+informing nearby iOS/macOS devices of the local IPv4 network address of
+the UxPlay server, which they can use to contact it on TCP port 7000.
+Instructions for manually setting up such a beacon in Linux are given below. It is hoped
+that users will submit Pull Requests contributing scripts for automating
+beacon setup on all platforms. (Python may be an appropriate language
+choice)
NEW on github: (for Linux/*BSD Desktop
Environments using D-Bus). New option -scrsv <n>
provides screensaver inhibition (e.g., to prevent screensaver function
@@ -217,8 +230,10 @@ necessary, it is not necessary that the local network also be of the
“.local” mDNS-based type). On Linux and BSD Unix servers, this is
usually provided by Avahi, through
the avahi-daemon service, and is included in most Linux distributions
-(this service can also be provided by macOS, iOS or Windows
-servers).
+(this service can also be provided by macOS, iOS or Windows servers).
+There is now an alternative Service discovery method, using a Bluetooth
+LE “beacon” See below for instructions.
Connections to the UxPlay server by iOS/MacOS clients can be
initiated both in AirPlay Mirror mode (which streams
lossily-compressed AAC audio while mirroring the client screen, or in
@@ -549,7 +564,8 @@ comments and ignored.
you can specify fullscreen mode with the -fs option, or
toggle into and out of fullscreen mode with F11 or (held-down left
Alt)+Enter keys. Use Ctrl-C (or close the window) to terminate it when
-done. If the UxPlay server is not seen by the iOS client’s drop-down
+done.
+If the UxPlay server is not seen by the iOS client’s drop-down
“Screen Mirroring” panel, check that your DNS-SD server (usually
avahi-daemon) is running: do this in a terminal window with
systemctl status avahi-daemon. If this shows the
@@ -564,6 +580,9 @@ opened: if a firewall is active, also open UDP port 5353 (for
mDNS queries) needed by Avahi. See Troubleshooting below for help with this or
other problems.
+Note that there is now an alternative Service Discovery method using
+a Bluetooth LE beacon. See the instructions on Bluetooth beacon setup.
Unlike an Apple TV, the UxPlay server does not by default require
clients to initially “pair” with it using a pin code displayed by the
@@ -1405,6 +1424,12 @@ to a file to n or less. To change the name audiodump,
use -admp [n] filename. Note that (unlike dumped video) the
dumped audio is currently only useful for debugging, as it is not
containerized to make it playable with standard audio players.
+-ble filename. Enable Bluetooth beacon
+Service Discovery. The PID and process name of the UxPlay process is
+recorded in filename, which must be the full path to a
+writeable file. (This file is created when UxPlay starts and deleted
+when it stops.) See below for beacon setup
+instructions.
-d [n] Enable debug output; optional argument n=1
suppresses audio/video packet data in debug output. Note: this does not
show GStreamer error or debug messages. To see GStreamer error and
@@ -1412,6 +1437,209 @@ warning messages, set the environment variable GST_DEBUG with “export
GST_DEBUG=2” before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
+Bluetooth LE beacon setup
+When uxplay is started with the option
+uxplay -ble <path-to-writeable-file>, it writes a 20
+byte data file containing (4 bytes) the process ID (PID) as a uint32_t
+32-bit unsigned integer, and (16 bytes) up to 15 bytes of the process
+name (usually “uxplay”) as a null-terminated string, padded with zeroes
+to fill 16 bytes. The file is deleted if UxPlay is terminated normally
+(without a segfault), and could be used to determine if an instance of
+uxplay is running. This file is provided for possible future use
+in a script for controlling the beacon, and will not be used
+here.
+You may need to use a cheap USB Bluetooth dongle if your system does
+not have Bluetooth 4.0 or later, or will not let you use it for LE (Low
+Energy) transmissions.
+These instructions are tested on Linux using the Bluez Bluetooth
+stack. They use the hcitool and hciconfig
+utilities which directly access the HCI stack, and need elevated
+privileges (use sudo). These utilities have been declared
+“deprecated” and “obsolete” by BlueZ developers: on Debian-based Linux
+sudo apt install bluez still provides hcitool,
+but on some other Linux distributions, it is split off from the main
+BlueZ package into an “extra” package with a name like
+“bluez-deprecated”. If we get the AirPlay beacon to work using the newer
+bluetoothctl utility, these instructions will be
+updated.
+
+These manual instructions will hopefully be soon
+superseded by e.g. python scripts that automate beacon control, probably
+using D-Bus on Linux. Please submit any such scripts you get working for
+possible packaging together with UxPlay. Note that the Apple
+Service Discovery beacon is not a standard “ibeacon”,
+and cannot be set up with unmodified “ibeacon”-specific
+applications.
+For testing Bluetooth beacon Service Discovery on Linux, you will
+need to suppress the avahi-daemon which provides DNS-SD Service
+Discovery on UxPlay’s Host system (replace mask and
+stop below by unmask and start to
+restore DNS-SD service).;
+
+$ sudo systemctl mask avahi-daemon.socket
+$ sudo systemctl stop avahi-daemon
+
+Then verify that uxplay will not start without the
+-ble <filename> option.
+Before starting, check that you have a Bluetooth device with
+“hcitool dev”
+$hcitool dev
+Devices:
+ hci1 E8:EA:6A:7C:3F:CC
+ hci0 08:BE:AC:40:A9:DC
+This shows two devices with their MAC addresses. You can use
+“hciconfig -i” to see which version of Bluetooth they
+implement: we require Bluetooth v4.0 or later. Choose which to use (we
+will use hci0), and reset it.
+$ sudo hciconfig hci0 reset
+
+
+- Step 1. First reconfigure the Bluetooth device
+(hci0):
+
+hcitool sends HCI commands as a sequence of 1-byte
+hexadecimal octets. It echoes the length (here plen = 15
+bytes) and content of the sequence it sends to the Bluetooth HCI stack,
+and of the 4-byte “HCI Event” response it gets. Only the last byte of
+the response is important: 00 means the command succeded
+(other values are error codes).
+
+$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
+
+< HCI Command: ogf 0x08, ocf 0x0006, plen 15
+ A0 00 A0 00 03 01 00 00 00 00 00 00 00 07 00
+> HCI Event: 0x0e plen 4
+ 02 06 20 00
+
+The above command configures the beacon:
+“cmd 0x08 0x006” means HCI LE (ogf=0x08) command number 6
+(ocf=0x0006) of the Blutooth LE stack.
+The first two message bytes “0xa0 0x00” means a 2-byte
+“unsigned short” value 0x00a0. (uint16_t integers such as 0xabcd are
+represented as two bytes “0xcd, 0xab”). This is the minimum
+interval AdvMin between beacon broadcasts, which are essentially
+simultaneous on all three advertising channels. The next two entries
+represent the maximum interval AdvMax, also set to 0x00a0, which means
+100 msec (200 msec would be 2 * 0x00a0 = 0x0140 or
+“0x40 0x01”). Setting AdvMin = AdvMax fixes the interval
+between transmissions. If AdvMin < AdvMax, the timing of each
+broadcast event relative to the previous one can be chosen flexibly to
+not overlap with any other task the bluetooth socket is carrying out.
+The allowed range of these parameters is 0x00a0 = 100 msec <= AdvMin
+<= AdvMax <= 0x4000 = 10.24 sec.
+An Apple TV (Gen 3) seems to use a fixed interval of 180 msec =
+0x0120 (“0x20 0x01”).
+The sixth byte TxAdd = “0x01” says that a random MAC
+“advertisement address”” AdvAddr for the Bluetooth device will be sent
+with the advertisement. If you wish to send the true hardware MAC
+address of the Bluetooth device, replace this byte by
+“0x00”.
+These are the only parameters you might want to
+vary. The fifth byte 0x03 is the Advertising PDU type
+“ADV_NONCONN_IND” (a beacon that transmits without accepting
+connections) and the fourteenth byte 0x07 is a flag 0000 0111 that says
+to use all three Bluetooth LE advertising channels.
+
+- Step 2. (Optional: skip this if you changed
+byte 6 of the initial configuration message from
+“
0x01” to “0x00”.) Use HCI LE
+command 5 (ocf=0x0005) to set private “advertising address” AdvAddr,
+which substitutes for the public MAC address of the Bluetooth
+device.
+
+This uses six random bytes r1,..,r6 and enters them as
+r1 , r2 , r3, r4, r5, r0 = (r6 | 0x03), where the 6th
+byte has been masked with 0x03 = 00000011 so its last two bits are on,
+and the value r0 is restricted to 64 values “0xrs” where
+the second hexadecimal digit s is one of {3, 7, b, f},
+which indicates a “static random” private address that is guaranteed to
+not change between device reboots. Note that Apple TV’s use random
+private addresses without applying a mask to r6 to distinguish between
+different types.
+
+$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
+< HCI Command: ogf 0x08, ocf 0x0005, plen 6
+ 52 AA AA 3A B4 2F
+> HCI Event: 0x0e plen 4
+ 02 05 20 00
+On a Bluetooth packet sniffer with wireshark, this address displays
+as: Advertising Address: 2f:b4:3a:aa:aa:52
+
+- Step 3. Now provide the advertising message, with
+HCI LE command 8 (ocf=0x0008):
+
+This sends a 32 byte message to the HCI LE stack, where the first
+byte is the length (here 0x0c = 12 bytes) of the significant part of the
+following 31 bytes: 12 significant bytes, padded with 19 zeros to a
+total message length of 32 bytes. (hcitool requires a
+message padded to the full 32 bytes, but only sends the significant
+bytes to the Bluetooth LE stack.)
+$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+< HCI Command: ogf 0x08, ocf 0x0008, plen 32
+ 0C 0B FF 4C 00 09 06 03 30 C0 A8 01 FD 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 00 00 00 00 00
+> HCI Event: 0x0e plen 4
+ 01 08 20 00
+The only parts of this message that you must change are the four
+bytes 10,11,12,13, of the IPv4 address, here
+“0xc0 0xa8 0x01 0xfd”, (decimal 192 168 1 253, an IPv4
+address 192.168.1.253) which should be an IPv4 address at which the
+UxPlay server can receive requests from iOS/macOS clients at TCP port
+7000. You need to find what IPv4 address will work on the computer that
+hosts UxPlay (use ifconfig), convert each of the four
+numbers from decimal to hexadecimal, and replace bytes 13-16 of the
+message by them.
+
+- Step 4. Start advertising by the beacon with
+Bluetooth LE command 10 (ocf = 0x000a) and 1-byte message
+“
0x01” = “on”.
+
+$ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
+< HCI Command: ogf 0x08, ocf 0x000a, plen 1
+ 01
+> HCI Event: 0x0e plen 4
+ 02 0A 20 00
+(To stop advertising, use this command to send the 1-byte message
+“0x00” = “off”.)
+For creating a higher-level script, it might be useful to know that
+the length 0C = 12 bytes advertisement sent in step 3 has a single
+“Advertising Protocol Data Unit” (PDU):
+
+- 0B FF 4C 00 09 06 03 30 C0 A8 01 FD: length 0B = 11 bytes,
+consisting of: FF ( type = manufacturer-specific) 4C 00 (manufacturer
+code = 0x004c, Apple ) manufacturer data 09 06 03 30 C0 A8 01 FD
+
+The manufacturer data defined by Apple consists of a single Apple
+data unit: 09 (Apple type = AirPlay), 06 (Apple data length 6 bytes)
+Apple data 03 30 XX XX XX XX, broken down into 03 (flags: 0000 0011) 30
+(a seed) XX XX XX XX (IPv4 network address, written as four hexadecimal
+octets in standard order). (Apple TV’s use a random private “AdvAddr”
+address as described above, and periodically update it at about 20 min
+intervals, each time increasing the seed by 1.)
+Apple TV’s also insert a type-1 (“Flags”) 2-byte PDU
+“02 01 1A” before the manufacturer-specific PDU, increasing
+the significant length of the message to 0xf = 15 bytes. It turns out
+that the “Flags” PDU is “optional” for advertisements like beacons that
+do not allow client connections: in our tests on v4.0 and later dongles,
+Service Discovery still worked fine after dropping the “Flags” PDU.
+Both Linux and Windows have high-level interfaces that support users
+sending Advertising PDU’s, but restricted to type 0xff
+“manufacturer-specific-data” only, without any “Flags”. These should be
+used for automating beacon setup, and are: (Linux) Bluez LEAdvertisingManager1
+and (Windows 10/11) BluetoothLEAdvertisementPublisherClass
+(with an example).
+We don’t know if these instructions can be modified to
+advertise IPv6 addresses: if you know of any verified support for
+Bluetooth LE IPv6 Service Discovery in newer AppleTV models, please let
+us know. Simply replacing the 4-byte IPv4 address with a 16-byte IPv6
+address (and adjusting the lengths at bytes 1, 5 and 10) does not seem
+to work, although perhaps we did not find the right value for byte 11
+(“Apple Flags”). If Apple’s Bluetooth LE Service Discovery has IPv6
+support, we need to examine the beacon advertisement packet for IPv6
+addresses with a Bluetooth sniffer.
Troubleshooting
Note: uxplay is run from a terminal command line, and
informational messages are written to the terminal.
@@ -1743,7 +1971,8 @@ what version UxPlay claims to be.
Changelog
xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option
(no file specified). (D-Bus based) option -scrsv to inhibit
-screensaver while UxPlay is running (Linux/*BSD only).
+screensaver while UxPlay is running (Linux/*BSD only). Add support for
+Service Discovery using a Bluetooth LE beacon.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced
with -pw option. Update llhttp to v 9.3.0
1.72.1 2025-06-06 minor update: fix regression in -reg option; add
diff --git a/README.md b/README.md
index 7ee7542..a556426 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,14 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
+- **NEW on github**: Support for **service discovery using a Bluetooth LE "beacon"** (as an alternative to Bonjour/Rendezvous DNS-SD
+ service discovery). The user must set up a Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used). See instructions
+ below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 44 byte packet informing nearby iOS/macOS devices of
+ the local IPv4 network address of the UxPlay server, which they can use to contact it on TCP port 7000.
+ Instructions for manually setting up such a beacon in Linux are [given below](#bluetooth-le-beacon-setup).
+ __It is hoped that users will submit Pull Requests contributing scripts for automating beacon setup on all platforms.
+ (Python may be an appropriate language choice)__
+
- **NEW on github**: (for Linux/*BSD Desktop Environments using D-Bus). New option `-scrsv ` provides screensaver inhibition (e.g., to
prevent screensaver function while watching mirrored videos without keyboard or mouse
activity): n = 0 (off) n=1 (on during video activity) n=2 (always on while UxPlay is running).
@@ -201,7 +209,10 @@ necessary that the local network also be of the ".local" mDNS-based
type). On Linux and BSD Unix servers, this is usually provided by
[Avahi](https://www.avahi.org), through the avahi-daemon service, and is
included in most Linux distributions (this service can also be provided
-by macOS, iOS or Windows servers).
+by macOS, iOS or Windows servers). There is now an alternative Service
+discovery method, using a Bluetooth LE "beacon" See below
+for [instructions](#bluetooth-le-beacon-setup).
+
Connections to the UxPlay server by iOS/MacOS clients can be initiated
both in **AirPlay Mirror** mode (which streams lossily-compressed AAC
@@ -532,7 +543,9 @@ as comments and ignored.
**Run uxplay in a terminal window**. On some systems, you can specify
fullscreen mode with the `-fs` option, or toggle into and out of
fullscreen mode with F11 or (held-down left Alt)+Enter keys. Use Ctrl-C
-(or close the window) to terminate it when done. If the UxPlay server is
+(or close the window) to terminate it when done.
+
+If the UxPlay server is
not seen by the iOS client's drop-down "Screen Mirroring" panel, check
that your DNS-SD server (usually avahi-daemon) is running: do this in a
terminal window with `systemctl status avahi-daemon`. If this shows the
@@ -547,6 +560,10 @@ opened: **if a firewall is active, also open UDP port 5353 (for mDNS
queries) needed by Avahi**. See [Troubleshooting](#troubleshooting)
below for help with this or other problems.
+Note that there is now an
+alternative Service Discovery method using a Bluetooth LE beacon.
+See the instructions on [Bluetooth beacon setup](#bluetooth-le-beacon-setup).
+
- Unlike an Apple TV, the UxPlay server does not by default require
clients to initially "pair" with it using a pin code displayed by
the server (after which the client "trusts" the server, and does not
@@ -1424,6 +1441,12 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
+**-ble *filename***. Enable Bluetooth beacon Service Discovery.
+The PID and process name of the UxPlay process is recorded in
+*filename*, which must be the full path to a writeable file. (This file is created
+when UxPlay starts and deleted when it stops.) __See below for beacon setup
+instructions.__
+
**-d \[n\]** Enable debug output; optional argument n=1 suppresses audio/video
packet data in debug output.
Note: this does not show GStreamer error or
@@ -1433,8 +1456,181 @@ uxplay. To see GStreamer information messages, set GST_DEBUG=4; for
DEBUG messages, GST_DEBUG=5; increase this to see even more of the
GStreamer inner workings.
-# Troubleshooting
+# Bluetooth LE beacon setup
+When uxplay is started with the option `uxplay -ble `, it writes a 20 byte data file
+containing (4 bytes) the process ID (PID) as a uint32_t 32-bit unsigned integer, and
+(16 bytes) up to 15 bytes of the process name (usually "uxplay") as a null-terminated string,
+padded with zeroes to fill 16 bytes. The file is deleted if UxPlay is terminated normally (without a segfault),
+and could be used to determine if an instance of uxplay is running. **This file is provided for possible future use
+in a script for controlling the beacon, and will not be used here**.
+
+You may need to use a cheap USB Bluetooth dongle if your system does not have Bluetooth 4.0 or later,
+or will not let you use it for LE (Low Energy) transmissions.
+
+
+These instructions are tested on Linux using the Bluez Bluetooth stack. They use the `hcitool` and ``hciconfig``
+utilities which directly access the HCI stack, and need elevated privileges (use `sudo`). These utilities
+have been declared "deprecated" and "obsolete" by BlueZ developers: on Debian-based Linux `sudo apt install bluez`
+still provides `hcitool`, but on some other Linux distributions, it is split off from the main BlueZ package into an "extra" package
+with a name like "bluez-deprecated". If we get the AirPlay beacon to work using the newer `bluetoothctl` utility, these instructions will be
+updated.
+
+* **These manual instructions will hopefully be soon superseded by e.g. python scripts that automate beacon control, probably using D-Bus on Linux. Please
+ submit any such scripts you get working for possible packaging together with UxPlay**. Note that the Apple Service Discovery beacon is
+ not a standard "**ibeacon**", and cannot be set up with unmodified "ibeacon"-specific applications.
+
+* For testing Bluetooth beacon Service Discovery on Linux, you will need to suppress the avahi-daemon which
+provides DNS-SD Service Discovery on UxPlay's Host system (replace `mask` and ``stop``
+below by `unmask` and ``start`` to restore DNS-SD service).;
+
+```
+$ sudo systemctl mask avahi-daemon.socket
+$ sudo systemctl stop avahi-daemon
+
+```
+Then verify that uxplay will not start without the `-ble ` option.
+
+
+
+Before starting, check that you have a Bluetooth device with "`hcitool dev`"
+
+```
+$hcitool dev
+Devices:
+ hci1 E8:EA:6A:7C:3F:CC
+ hci0 08:BE:AC:40:A9:DC
+```
+
+This shows two devices with their MAC addresses. You can use "`hciconfig -i`" to see which version of Bluetooth they
+implement: we require Bluetooth v4.0 or later. Choose which to use (we will use hci0), and reset it.
+
+```
+$ sudo hciconfig hci0 reset
+
+```
+
+* **Step 1.** First reconfigure the Bluetooth device (hci0):
+
+`hcitool` sends HCI commands as a sequence of 1-byte hexadecimal octets. It echoes the length (here `plen` = 15 bytes) and content
+of the sequence it sends to the Bluetooth HCI stack, and of the 4-byte "HCI Event" response it gets.
+Only the last byte of the response is important: `00` means the command succeded (other values are error codes).
+
+```
+
+$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
+
+< HCI Command: ogf 0x08, ocf 0x0006, plen 15
+ A0 00 A0 00 03 01 00 00 00 00 00 00 00 07 00
+> HCI Event: 0x0e plen 4
+ 02 06 20 00
+
+```
+
+The above command configures the beacon: "`cmd 0x08 0x006`" means HCI LE (ogf=0x08) command number 6 (ocf=0x0006) of
+the Blutooth LE stack.
+
+The first two message bytes "`0xa0 0x00`" means a 2-byte "unsigned short" value 0x00a0. (uint16_t integers such as 0xabcd are represented as two bytes "`0xcd, 0xab`"). This is the minimum interval AdvMin between
+beacon broadcasts, which are essentially simultaneous on all three advertising channels. The next two entries represent the maximum interval AdvMax, also set to 0x00a0,
+which means 100 msec (200 msec would be 2 * 0x00a0 = 0x0140 or "`0x40 0x01`"). Setting AdvMin = AdvMax fixes the interval between transmissions.
+If AdvMin < AdvMax, the timing of each broadcast event relative to the previous one can be chosen flexibly to not overlap with any other task the bluetooth socket is carrying out.
+The allowed range of these parameters is 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000 = 10.24 sec.
+
+An Apple TV (Gen 3) seems to use a fixed interval of 180 msec = 0x0120 ("`0x20 0x01`").
+
+The sixth byte TxAdd = "`0x01`" says that a random MAC "advertisement address"" AdvAddr for the Bluetooth device will be sent with the advertisement. If you wish to send the true
+hardware MAC address of the Bluetooth device, replace this byte by "`0x00`".
+
+
+__These are the only parameters you might want to vary__. The fifth byte 0x03 is the Advertising PDU type "ADV_NONCONN_IND" (a beacon that transmits without accepting connections)
+and the fourteenth byte 0x07 is a flag 0000 0111 that says to use all three Bluetooth LE advertising channels.
+
+
+
+* **Step 2.** (**Optional: skip this if you changed byte 6 of the initial configuration message from** "``0x01``" **to** "`0x00`".)
+Use HCI LE command 5 (ocf=0x0005) to
+set private "advertising address" AdvAddr, which substitutes for the public MAC address of the Bluetooth device.
+
+This uses six random bytes r1,..,r6 and enters them as
+` r1 , r2 , r3, r4, r5, r0 = (r6 | 0x03)`, where the 6th byte has been masked with 0x03 = 00000011 so its last two bits are on,
+and the value r0 is restricted to 64 values "`0xrs`" where the second
+hexadecimal digit ``s`` is one of {3, 7, b, f}, which indicates a "static random" private address that is guaranteed to not
+change between device reboots. Note that Apple TV's use random private addresses without applying a mask to r6 to distinguish
+between different types.
+
+
+```
+
+$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
+< HCI Command: ogf 0x08, ocf 0x0005, plen 6
+ 52 AA AA 3A B4 2F
+> HCI Event: 0x0e plen 4
+ 02 05 20 00
+```
+
+On a Bluetooth packet sniffer with wireshark, this address displays as: **Advertising Address: 2f:b4:3a:aa:aa:52**
+
+* **Step 3.** Now provide the advertising message, with HCI LE command 8 (ocf=0x0008):
+
+
+This sends a 32 byte message to the HCI LE stack, where the first byte is the length (here 0x0c = 12 bytes) of the significant part of
+the following 31 bytes: 12 significant bytes, padded with 19 zeros to a total message length of 32 bytes. (`hcitool` requires a message padded to the full
+32 bytes, but only sends the significant bytes to the Bluetooth LE stack.)
+
+```
+$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+< HCI Command: ogf 0x08, ocf 0x0008, plen 32
+ 0C 0B FF 4C 00 09 06 03 30 C0 A8 01 FD 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 00 00 00 00 00
+> HCI Event: 0x0e plen 4
+ 01 08 20 00
+```
+
+The only parts of this message that you must change are the four bytes 10,11,12,13, of the IPv4 address, here "` 0xc0 0xa8 0x01 0xfd `", (decimal 192 168 1 253, an IPv4 address 192.168.1.253)
+ which should be an IPv4 address at which the UxPlay server can receive requests from iOS/macOS clients at TCP port 7000. You need to find what IPv4 address will work
+on the computer that hosts UxPlay (use `ifconfig`), convert each of the four numbers from decimal to hexadecimal, and replace bytes 13-16 of the message by them.
+
+
+* **Step 4.** Start advertising by the beacon with Bluetooth LE command 10 (ocf = 0x000a) and 1-byte message "`0x01`" = "on".
+
+```
+$ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
+< HCI Command: ogf 0x08, ocf 0x000a, plen 1
+ 01
+> HCI Event: 0x0e plen 4
+ 02 0A 20 00
+```
+
+(To stop advertising, use this command to send the 1-byte message "`0x00`" = "off".)
+
+
+
+For creating a higher-level script, it might be useful to know that the length 0C = 12 bytes advertisement sent in step 3 has a single "Advertising Protocol Data Unit" (PDU):
+
+* 0B FF 4C 00 09 06 03 30 C0 A8 01 FD: length 0B = 11 bytes, consisting of: FF ( type = manufacturer-specific) 4C 00 (manufacturer code = 0x004c, Apple ) manufacturer data 09 06 03 30 C0 A8 01 FD
+
+The manufacturer data defined by Apple consists of a single Apple data unit: 09 (Apple type = AirPlay), 06 (Apple data length 6 bytes) Apple data 03 30 XX XX XX XX, broken down into 03 (flags: 0000 0011) 30 (a seed)
+XX XX XX XX (IPv4 network address, written as four hexadecimal octets in standard order). (Apple TV's use a random private "AdvAddr" address as described above, and periodically update it at about 20 min intervals, each time
+increasing the seed by 1.)
+
+Apple TV's also insert a type-1 ("Flags") 2-byte PDU "`02 01 1A`" before the manufacturer-specific PDU, increasing the significant length of the message to 0xf = 15 bytes.
+It turns out that the "Flags" PDU is "optional" for advertisements like beacons that do not allow client connections:
+in our tests on v4.0 and later dongles, Service Discovery still worked fine after dropping the "Flags" PDU.
+
+Both Linux and Windows have high-level interfaces that support users sending Advertising PDU's, but restricted to
+type 0xff "manufacturer-specific-data" only, without any "Flags". These should be used for automating beacon setup, and
+are: (Linux) Bluez [LEAdvertisingManager1](https://github.com/bluez/bluez/blob/master/test/example-advertisement)
+and (Windows 10/11) [BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
+(with an [example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
+
+
+**We don't know if these instructions can be modified to advertise IPv6 addresses: if you know of any verified support for Bluetooth LE IPv6 Service Discovery in newer AppleTV models,
+please let us know. Simply replacing the 4-byte IPv4 address with a 16-byte IPv6 address (and adjusting the lengths at bytes 1, 5 and 10) does not seem to work, although perhaps
+we did not find the right value for byte 11 ("Apple Flags"). If Apple's Bluetooth LE Service Discovery has IPv6 support, we need to examine the beacon advertisement packet
+for IPv6 addresses with a Bluetooth sniffer.**
+
+
+# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
messages are written to the terminal.
@@ -1776,7 +1972,8 @@ what version UxPlay claims to be.
# Changelog
xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option (no file
specified). (D-Bus based) option -scrsv to inhibit screensaver while UxPlay
-is running (Linux/*BSD only).
+is running (Linux/*BSD only). Add support for Service Discovery using a
+Bluetooth LE beacon.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with -pw
option. Update llhttp to v 9.3.0
diff --git a/README.txt b/README.txt
index e06690f..be5a657 100644
--- a/README.txt
+++ b/README.txt
@@ -2,6 +2,20 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
+- **NEW on github**: Support for **service discovery using a Bluetooth
+ LE "beacon"** (as an alternative to Bonjour/Rendezvous DNS-SD
+ service discovery). The user must set up a Bluetooth LE "beacon", (a
+ USB 4.0 or later "dongle" can be used). See instructions below. The
+ beacon runs independently of UxPlay and regularly broadcasts a
+ Bluetooth LE ("Low Energy") 44 byte packet informing nearby
+ iOS/macOS devices of the local IPv4 network address of the UxPlay
+ server, which they can use to contact it on TCP port 7000.
+ Instructions for manually setting up such a beacon in Linux are
+ [given below](#bluetooth-le-beacon-setup). **It is hoped that users
+ will submit Pull Requests contributing scripts for automating beacon
+ setup on all platforms. (Python may be an appropriate language
+ choice)**
+
- **NEW on github**: (for Linux/\*BSD Desktop Environments using
D-Bus). New option `-scrsv ` provides screensaver inhibition
(e.g., to prevent screensaver function while watching mirrored
@@ -219,7 +233,9 @@ necessary that the local network also be of the ".local" mDNS-based
type). On Linux and BSD Unix servers, this is usually provided by
[Avahi](https://www.avahi.org), through the avahi-daemon service, and is
included in most Linux distributions (this service can also be provided
-by macOS, iOS or Windows servers).
+by macOS, iOS or Windows servers). There is now an alternative Service
+discovery method, using a Bluetooth LE "beacon" See below for
+[instructions](#bluetooth-le-beacon-setup).
Connections to the UxPlay server by iOS/MacOS clients can be initiated
both in **AirPlay Mirror** mode (which streams lossily-compressed AAC
@@ -550,11 +566,13 @@ as comments and ignored.
**Run uxplay in a terminal window**. On some systems, you can specify
fullscreen mode with the `-fs` option, or toggle into and out of
fullscreen mode with F11 or (held-down left Alt)+Enter keys. Use Ctrl-C
-(or close the window) to terminate it when done. If the UxPlay server is
-not seen by the iOS client's drop-down "Screen Mirroring" panel, check
-that your DNS-SD server (usually avahi-daemon) is running: do this in a
-terminal window with `systemctl status avahi-daemon`. If this shows the
-avahi-daemon is not running, control it with
+(or close the window) to terminate it when done.
+
+If the UxPlay server is not seen by the iOS client's drop-down "Screen
+Mirroring" panel, check that your DNS-SD server (usually avahi-daemon)
+is running: do this in a terminal window with
+`systemctl status avahi-daemon`. If this shows the avahi-daemon is not
+running, control it with
`sudo systemctl [start,stop,enable,disable] avahi-daemon` (on
non-systemd systems, such as \*BSD, use
`sudo service avahi-daemon [status, start, stop, restart, ...]`). If
@@ -565,6 +583,10 @@ opened: **if a firewall is active, also open UDP port 5353 (for mDNS
queries) needed by Avahi**. See [Troubleshooting](#troubleshooting)
below for help with this or other problems.
+Note that there is now an alternative Service Discovery method using a
+Bluetooth LE beacon. See the instructions on [Bluetooth beacon
+setup](#bluetooth-le-beacon-setup).
+
- Unlike an Apple TV, the UxPlay server does not by default require
clients to initially "pair" with it using a pin code displayed by
the server (after which the client "trusts" the server, and does not
@@ -1443,6 +1465,12 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
+**-ble *filename***. Enable Bluetooth beacon Service Discovery. The PID
+and process name of the UxPlay process is recorded in *filename*, which
+must be the full path to a writeable file. (This file is created when
+UxPlay starts and deleted when it stops.) **See below for beacon setup
+instructions.**
+
**-d \[n\]** Enable debug output; optional argument n=1 suppresses
audio/video packet data in debug output. Note: this does not show
GStreamer error or debug messages. To see GStreamer error and warning
@@ -1451,6 +1479,225 @@ GST_DEBUG=2" before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
+# Bluetooth LE beacon setup
+
+When uxplay is started with the option
+`uxplay -ble `, it writes a 20 byte data file
+containing (4 bytes) the process ID (PID) as a uint32_t 32-bit unsigned
+integer, and (16 bytes) up to 15 bytes of the process name (usually
+"uxplay") as a null-terminated string, padded with zeroes to fill 16
+bytes. The file is deleted if UxPlay is terminated normally (without a
+segfault), and could be used to determine if an instance of uxplay is
+running. **This file is provided for possible future use in a script for
+controlling the beacon, and will not be used here**.
+
+You may need to use a cheap USB Bluetooth dongle if your system does not
+have Bluetooth 4.0 or later, or will not let you use it for LE (Low
+Energy) transmissions.
+
+These instructions are tested on Linux using the Bluez Bluetooth stack.
+They use the `hcitool` and `hciconfig` utilities which directly access
+the HCI stack, and need elevated privileges (use `sudo`). These
+utilities have been declared "deprecated" and "obsolete" by BlueZ
+developers: on Debian-based Linux `sudo apt install bluez` still
+provides `hcitool`, but on some other Linux distributions, it is split
+off from the main BlueZ package into an "extra" package with a name like
+"bluez-deprecated". If we get the AirPlay beacon to work using the newer
+`bluetoothctl` utility, these instructions will be updated.
+
+- **These manual instructions will hopefully be soon superseded by
+ e.g. python scripts that automate beacon control, probably using
+ D-Bus on Linux. Please submit any such scripts you get working for
+ possible packaging together with UxPlay**. Note that the Apple
+ Service Discovery beacon is not a standard "**ibeacon**", and cannot
+ be set up with unmodified "ibeacon"-specific applications.
+
+- For testing Bluetooth beacon Service Discovery on Linux, you will
+ need to suppress the avahi-daemon which provides DNS-SD Service
+ Discovery on UxPlay's Host system (replace `mask` and `stop` below
+ by `unmask` and `start` to restore DNS-SD service).;
+
+```{=html}
+
+```
+ $ sudo systemctl mask avahi-daemon.socket
+ $ sudo systemctl stop avahi-daemon
+
+Then verify that uxplay will not start without the `-ble `
+option.
+
+Before starting, check that you have a Bluetooth device with
+"`hcitool dev`"
+
+ $hcitool dev
+ Devices:
+ hci1 E8:EA:6A:7C:3F:CC
+ hci0 08:BE:AC:40:A9:DC
+
+This shows two devices with their MAC addresses. You can use
+"`hciconfig -i`" to see which version of Bluetooth they implement: we
+require Bluetooth v4.0 or later. Choose which to use (we will use hci0),
+and reset it.
+
+ $ sudo hciconfig hci0 reset
+
+- **Step 1.** First reconfigure the Bluetooth device (hci0):
+
+`hcitool` sends HCI commands as a sequence of 1-byte hexadecimal octets.
+It echoes the length (here `plen` = 15 bytes) and content of the
+sequence it sends to the Bluetooth HCI stack, and of the 4-byte "HCI
+Event" response it gets. Only the last byte of the response is
+important: `00` means the command succeded (other values are error
+codes).
+
+
+ $ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
+
+ < HCI Command: ogf 0x08, ocf 0x0006, plen 15
+ A0 00 A0 00 03 01 00 00 00 00 00 00 00 07 00
+ > HCI Event: 0x0e plen 4
+ 02 06 20 00
+
+The above command configures the beacon: "`cmd 0x08 0x006`" means HCI LE
+(ogf=0x08) command number 6 (ocf=0x0006) of the Blutooth LE stack.
+
+The first two message bytes "`0xa0 0x00`" means a 2-byte "unsigned
+short" value 0x00a0. (uint16_t integers such as 0xabcd are represented
+as two bytes "`0xcd, 0xab`"). This is the minimum interval AdvMin
+between beacon broadcasts, which are essentially simultaneous on all
+three advertising channels. The next two entries represent the maximum
+interval AdvMax, also set to 0x00a0, which means 100 msec (200 msec
+would be 2 \* 0x00a0 = 0x0140 or "`0x40 0x01`"). Setting AdvMin = AdvMax
+fixes the interval between transmissions. If AdvMin \< AdvMax, the
+timing of each broadcast event relative to the previous one can be
+chosen flexibly to not overlap with any other task the bluetooth socket
+is carrying out. The allowed range of these parameters is 0x00a0 = 100
+msec \<= AdvMin \<= AdvMax \<= 0x4000 = 10.24 sec.
+
+An Apple TV (Gen 3) seems to use a fixed interval of 180 msec = 0x0120
+("`0x20 0x01`").
+
+The sixth byte TxAdd = "`0x01`" says that a random MAC "advertisement
+address"" AdvAddr for the Bluetooth device will be sent with the
+advertisement. If you wish to send the true hardware MAC address of the
+Bluetooth device, replace this byte by "`0x00`".
+
+**These are the only parameters you might want to vary**. The fifth byte
+0x03 is the Advertising PDU type "ADV_NONCONN_IND" (a beacon that
+transmits without accepting connections) and the fourteenth byte 0x07 is
+a flag 0000 0111 that says to use all three Bluetooth LE advertising
+channels.
+
+- **Step 2.** (**Optional: skip this if you changed byte 6 of the
+ initial configuration message from** "`0x01`" **to** "`0x00`".) Use
+ HCI LE command 5 (ocf=0x0005) to set private "advertising address"
+ AdvAddr, which substitutes for the public MAC address of the
+ Bluetooth device.
+
+This uses six random bytes r1,..,r6 and enters them as
+`r1 , r2 , r3, r4, r5, r0 = (r6 | 0x03)`, where the 6th byte has been
+masked with 0x03 = 00000011 so its last two bits are on, and the value
+r0 is restricted to 64 values "`0xrs`" where the second hexadecimal
+digit `s` is one of {3, 7, b, f}, which indicates a "static random"
+private address that is guaranteed to not change between device reboots.
+Note that Apple TV's use random private addresses without applying a
+mask to r6 to distinguish between different types.
+
+
+ $sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
+ < HCI Command: ogf 0x08, ocf 0x0005, plen 6
+ 52 AA AA 3A B4 2F
+ > HCI Event: 0x0e plen 4
+ 02 05 20 00
+
+On a Bluetooth packet sniffer with wireshark, this address displays as:
+**Advertising Address: 2f:b4:3a:aa:aa:52**
+
+- **Step 3.** Now provide the advertising message, with HCI LE command
+ 8 (ocf=0x0008):
+
+This sends a 32 byte message to the HCI LE stack, where the first byte
+is the length (here 0x0c = 12 bytes) of the significant part of the
+following 31 bytes: 12 significant bytes, padded with 19 zeros to a
+total message length of 32 bytes. (`hcitool` requires a message padded
+to the full 32 bytes, but only sends the significant bytes to the
+Bluetooth LE stack.)
+
+ $ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+ < HCI Command: ogf 0x08, ocf 0x0008, plen 32
+ 0C 0B FF 4C 00 09 06 03 30 C0 A8 01 FD 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 00 00 00 00 00
+ > HCI Event: 0x0e plen 4
+ 01 08 20 00
+
+The only parts of this message that you must change are the four bytes
+10,11,12,13, of the IPv4 address, here "`0xc0 0xa8 0x01 0xfd`", (decimal
+192 168 1 253, an IPv4 address 192.168.1.253) which should be an IPv4
+address at which the UxPlay server can receive requests from iOS/macOS
+clients at TCP port 7000. You need to find what IPv4 address will work
+on the computer that hosts UxPlay (use `ifconfig`), convert each of the
+four numbers from decimal to hexadecimal, and replace bytes 13-16 of the
+message by them.
+
+- **Step 4.** Start advertising by the beacon with Bluetooth LE
+ command 10 (ocf = 0x000a) and 1-byte message "`0x01`" = "on".
+
+```{=html}
+
+```
+ $ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
+ < HCI Command: ogf 0x08, ocf 0x000a, plen 1
+ 01
+ > HCI Event: 0x0e plen 4
+ 02 0A 20 00
+
+(To stop advertising, use this command to send the 1-byte message
+"`0x00`" = "off".)
+
+For creating a higher-level script, it might be useful to know that the
+length 0C = 12 bytes advertisement sent in step 3 has a single
+"Advertising Protocol Data Unit" (PDU):
+
+- 0B FF 4C 00 09 06 03 30 C0 A8 01 FD: length 0B = 11 bytes,
+ consisting of: FF ( type = manufacturer-specific) 4C 00
+ (manufacturer code = 0x004c, Apple ) manufacturer data 09 06 03 30
+ C0 A8 01 FD
+
+The manufacturer data defined by Apple consists of a single Apple data
+unit: 09 (Apple type = AirPlay), 06 (Apple data length 6 bytes) Apple
+data 03 30 XX XX XX XX, broken down into 03 (flags: 0000 0011) 30 (a
+seed) XX XX XX XX (IPv4 network address, written as four hexadecimal
+octets in standard order). (Apple TV's use a random private "AdvAddr"
+address as described above, and periodically update it at about 20 min
+intervals, each time increasing the seed by 1.)
+
+Apple TV's also insert a type-1 ("Flags") 2-byte PDU "`02 01 1A`" before
+the manufacturer-specific PDU, increasing the significant length of the
+message to 0xf = 15 bytes. It turns out that the "Flags" PDU is
+"optional" for advertisements like beacons that do not allow client
+connections: in our tests on v4.0 and later dongles, Service Discovery
+still worked fine after dropping the "Flags" PDU.
+
+Both Linux and Windows have high-level interfaces that support users
+sending Advertising PDU's, but restricted to type 0xff
+"manufacturer-specific-data" only, without any "Flags". These should be
+used for automating beacon setup, and are: (Linux) Bluez
+[LEAdvertisingManager1](https://github.com/bluez/bluez/blob/master/test/example-advertisement)
+and (Windows 10/11)
+[BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
+(with an
+[example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
+
+**We don't know if these instructions can be modified to advertise IPv6
+addresses: if you know of any verified support for Bluetooth LE IPv6
+Service Discovery in newer AppleTV models, please let us know. Simply
+replacing the 4-byte IPv4 address with a 16-byte IPv6 address (and
+adjusting the lengths at bytes 1, 5 and 10) does not seem to work,
+although perhaps we did not find the right value for byte 11 ("Apple
+Flags"). If Apple's Bluetooth LE Service Discovery has IPv6 support, we
+need to examine the beacon advertisement packet for IPv6 addresses with
+a Bluetooth sniffer.**
+
# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
@@ -1795,7 +2042,8 @@ what version UxPlay claims to be.
xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option (no
file specified). (D-Bus based) option -scrsv ``{=html} to inhibit
-screensaver while UxPlay is running (Linux/\*BSD only).
+screensaver while UxPlay is running (Linux/\*BSD only). Add support for
+Service Discovery using a Bluetooth LE beacon.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with
-pw option. Update llhttp to v 9.3.0
diff --git a/lib/raop.c b/lib/raop.c
index 80bb996..ee0c5e4 100644
--- a/lib/raop.c
+++ b/lib/raop.c
@@ -188,26 +188,35 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
All requests arriving here have been parsed by llhttp to obtain
method | url | protocol (RTSP/1.0 or HTTP/1.1)
- There are three types of connections supplying these requests:
+ There are four types of connections supplying these requests:
Connections from the AirPlay client:
(1) type RAOP connections with CSeq seqence header, and no X-Apple-Session-ID header
(2) type AIRPLAY connection with an X-Apple-Sequence-ID header and no Cseq header
Connections from localhost:
(3) type HLS internal connections from the local HLS server (gstreamer) at localhost with neither
of these headers, but a Host: localhost:[port] header. method = GET.
+ (4) a special RAOP connection trigggered by a Bluetooth LE beacon: Protocol RTSP/1.0, method: GET
+ url /info?txtAirPlay?txtRAOP, and no headers including CSeq
*/
const char *method = http_request_get_method(request);
const char *url = http_request_get_url(request);
+ const char *protocol = http_request_get_protocol(request);
- if (!method || !url) {
+ if (!method || !url || !protocol) {
return;
}
-/* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
+ /* ¨idenitfy if request is a response to a BLE beaconn */
const char *cseq = http_request_get_header(request, "CSeq");
- const char *protocol = http_request_get_protocol(request);
- if (!cseq && !conn->raop->hls_support) {
+ bool ble = false;
+ if (!strcmp(protocol,"RTSP/1.0") && !cseq && (strstr(url, "txtAirPlay") || strstr(url, "txtRAOP") )) {
+ logger_log(conn->raop->logger, LOGGER_INFO, "response to Bluetooth LE beacon advertisement received)");
+ ble = true;
+ }
+
+ /* this rejects messages from _airplay._tcp for video streaming protocol unless bool raop->hls_support is true*/
+ if (!cseq && !conn->raop->hls_support && !ble) {
logger_log(conn->raop->logger, LOGGER_INFO, "ignoring AirPlay video streaming request (use option -hls to activate HLS support)");
return;
}
@@ -217,7 +226,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
hls_request = (host && !cseq && !client_session_id);
if (conn->connection_type == CONNECTION_TYPE_UNKNOWN) {
- if (cseq) {
+ if (cseq || ble) {
if (httpd_count_connection_type(conn->raop->httpd, CONNECTION_TYPE_RAOP)) {
char ipaddr[40];
utils_ipaddress_to_string(conn->remotelen, conn->remote, conn->zone_id, ipaddr, (int) (sizeof(ipaddr)));
@@ -367,7 +376,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
handler = &raop_handler_audiomode;
}
} else if (!strcmp(method, "GET")) {
- if (!strcmp(url, "/info")) {
+ if (strstr(url, "/info")) {
handler = &raop_handler_info;
}
} else if (!strcmp(method, "OPTIONS")) {
diff --git a/uxplay.1 b/uxplay.1
index 237d782..32bec7b 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -184,6 +184,8 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
audio packets are dumped. "aud"= unknown format.
.PP
.TP
+\fB\-ble\fI fn\fR For BluetoothLE beacon: write PID to file fn ("off" to cancel)
+.TP
\fB\-d [n]\fR Enable debug logging; optional: n=1 to skip normal packet data.
.TP
\fB\-v\fR Displays version information
diff --git a/uxplay.cpp b/uxplay.cpp
index 3b73e01..218d629 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -189,6 +189,7 @@ static uint32_t rtptime_end = 0;
static uint32_t rtptime_coverart_expired = 0;
static std::string artist;
static std::string coverart_artist;
+static std::string ble_filename = "";
//Support for D-Bus-based screensaver inhibition (org.freedesktop.ScreenSaver)
static unsigned int scrsv;
@@ -353,6 +354,18 @@ static size_t write_metadata(const char *filename, const char *text) {
return count;
}
+static int write_bledata( const uint32_t *pid, const char *process_name, const char *filename) {
+ char name[16] { 0 };
+ size_t len = strlen(process_name);
+ memcpy (name, process_name, (len > 15 ? 15 :len));
+ FILE *fp = fopen(filename, "wb");
+ size_t count = fwrite(pid, sizeof (uint32_t), 1, fp);
+ count *= sizeof(uint32_t);
+ count += fwrite(name, 1, sizeof(name), fp);
+ fclose(fp);
+ return (int) count;
+}
+
static char *create_pin_display(char *pin_str, int margin, int gap) {
char *ptr;
char num[2] = { 0 };
@@ -905,6 +918,7 @@ static void print_info (char *name) {
printf(" =1,2,..; fn=\"audiodump\"; change with \"-admp [n] filename\".\n");
printf(" x increases when audio format changes. If n is given, <= n\n");
printf(" audio packets are dumped. \"aud\"= unknown format.\n");
+ printf("-ble fn For BluetoothLE beacon: write PID to file fn (\"off\" to cancel)\n");
printf("-d [n] Enable debug logging; optional: n=1 to skip normal packet data\n");
printf("-v Displays version information\n");
printf("-h Displays this help\n");
@@ -1357,6 +1371,21 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr,"option -md must be followed by a filename for metadata text output\n");
exit(1);
}
+ } else if (arg == "-ble" ) {
+ if (option_has_value(i, argc, arg, argv[i+1])) {
+ ble_filename.erase();
+ i++;
+ if (strlen(argv[i]) != 3 || strncmp(argv[i], "off", 3)) {
+ ble_filename.append(argv[i]);
+ if (!file_has_write_access(argv[i])) {
+ fprintf(stderr, "%s cannot be written to:\noption \"-ble\" must be to a file with write access\n", argv[i]);
+ exit(1);
+ }
+ }
+ } else {
+ fprintf(stderr,"option -ble must be followed by a filename for PID data or by \"off\"\n");
+ exit(1);
+ }
} else if (arg == "-bt709") {
bt709_fix = true;
} else if (arg == "-srgb") {
@@ -1678,26 +1707,37 @@ static int register_dnssd() {
int dnssd_error;
uint64_t features;
- if ((dnssd_error = dnssd_register_raop(dnssd, raop_port))) {
- if (dnssd_error == -65537) {
- LOGE("No DNS-SD Server found (DNSServiceRegister call returned kDNSServiceErr_Unknown)");
- } else if (dnssd_error == -65548) {
- LOGE("DNSServiceRegister call returned kDNSServiceErr_NameConflict");
- LOGI("Is another instance of %s running with the same DeviceID (MAC address) or using same network ports?",
- DEFAULT_NAME);
- LOGI("Use options -m ... and -p ... to allow multiple instances of %s to run concurrently", DEFAULT_NAME);
+ dnssd_error = dnssd_register_raop(dnssd, raop_port);
+ if (dnssd_error) {
+ if (ble_filename.empty()) {
+ if (dnssd_error == -65537) {
+ LOGE("No DNS-SD Server found (DNSServiceRegister call returned kDNSServiceErr_Unknown)");
+ } else if (dnssd_error == -65548) {
+ LOGE("DNSServiceRegister call returned kDNSServiceErr_NameConflict");
+ LOGI("Is another instance of %s running with the same DeviceID (MAC address) or using same network ports?",
+ DEFAULT_NAME);
+ LOGI("Use options -m ... and -p ... to allow multiple instances of %s to run concurrently", DEFAULT_NAME);
+ } else {
+ LOGE("dnssd_register_raop failed with error code %d\n"
+ "mDNS Error codes are in range FFFE FF00 (-65792) to FFFE FFFF (-65537) "
+ "(see Apple's dns_sd.h)", dnssd_error);
+ }
+ return -3;
} else {
- LOGE("dnssd_register_raop failed with error code %d\n"
- "mDNS Error codes are in range FFFE FF00 (-65792) to FFFE FFFF (-65537) "
- "(see Apple's dns_sd.h)", dnssd_error);
+ LOGI("dnssd_register_raop failed: ignoring because Bluetooth LE service discovery may be available");
}
- return -3;
}
- if ((dnssd_error = dnssd_register_airplay(dnssd, airplay_port))) {
- LOGE("dnssd_register_airplay failed with error code %d\n"
- "mDNS Error codes are in range FFFE FF00 (-65792) to FFFE FFFF (-65537) "
- "(see Apple's dns_sd.h)", dnssd_error);
- return -4;
+
+ dnssd_error = dnssd_register_airplay(dnssd, airplay_port);
+ if (dnssd_error) {
+ if (ble_filename.empty()) {
+ LOGE("dnssd_register_airplay failed with error code %d\n"
+ "mDNS Error codes are in range FFFE FF00 (-65792) to FFFE FFFF (-65537) "
+ "(see Apple's dns_sd.h)", dnssd_error);
+ return -4;
+ } else {
+ LOGI("dnssd_register_airplay failed: ignoring because Bluetooth LE service discovery may be available");
+ }
}
LOGD("register_dnssd: advertised AirPlay service with \"Features\" code = 0x%llX",
@@ -2818,6 +2858,19 @@ int main (int argc, char *argv[]) {
write_metadata(metadata_filename.c_str(), "no data\n");
}
+#define PID_MAX 4194304 // 2^22
+ if (ble_filename.length()) {
+#ifdef _WIN_32
+ DWORD pid = GetCurrentProcessId();
+ g_assert(pid <= PID_MAX);
+#else
+ pid_t pid = getpid();
+ g_assert (pid <= PID_MAX && pid >= 0);
+#endif
+ write_bledata((uint32_t *) &pid, argv[0], ble_filename.c_str());
+ LOGI("Bluetooth LE beacon-based service discovery is possible: PID data written to %s", ble_filename.c_str());
+ }
+
/* set default resolutions for h264 or h265*/
if (!display[0] && !display[1]) {
if (h265_support) {
@@ -2895,10 +2948,13 @@ int main (int argc, char *argv[]) {
fclose(video_dumpfile);
}
if (coverart_filename.length()) {
- remove (coverart_filename.c_str());
+ remove (coverart_filename.c_str());
}
if (metadata_filename.length()) {
- remove (metadata_filename.c_str());
+ remove (metadata_filename.c_str());
+ }
+ if (ble_filename.length()) {
+ remove (ble_filename.c_str());
}
#ifdef DBUS
if (dbus_connection) {
From 151d4a7a3e9690f3fcf99a9ba6c9b1c0a6a1a2df Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 20 Sep 2025 17:46:22 -0400
Subject: [PATCH 41/58] Simplify the Blutooth LE beacon setup instructions
---
README.html | 314 +++++++++++++++++++++++-----------------------------
README.md | 217 +++++++++++++++++-------------------
README.txt | 302 ++++++++++++++++++++++----------------------------
3 files changed, 375 insertions(+), 458 deletions(-)
diff --git a/README.html b/README.html
index e9eb958..2779dd3 100644
--- a/README.html
+++ b/README.html
@@ -1438,208 +1438,172 @@ GST_DEBUG=2” before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
Bluetooth LE beacon setup
-When uxplay is started with the option
-uxplay -ble <path-to-writeable-file>, it writes a 20
-byte data file containing (4 bytes) the process ID (PID) as a uint32_t
-32-bit unsigned integer, and (16 bytes) up to 15 bytes of the process
-name (usually “uxplay”) as a null-terminated string, padded with zeroes
-to fill 16 bytes. The file is deleted if UxPlay is terminated normally
-(without a segfault), and could be used to determine if an instance of
-uxplay is running. This file is provided for possible future use
-in a script for controlling the beacon, and will not be used
-here.
-You may need to use a cheap USB Bluetooth dongle if your system does
-not have Bluetooth 4.0 or later, or will not let you use it for LE (Low
-Energy) transmissions.
-These instructions are tested on Linux using the Bluez Bluetooth
-stack. They use the hcitool and hciconfig
-utilities which directly access the HCI stack, and need elevated
-privileges (use sudo). These utilities have been declared
-“deprecated” and “obsolete” by BlueZ developers: on Debian-based Linux
-sudo apt install bluez still provides hcitool,
-but on some other Linux distributions, it is split off from the main
-BlueZ package into an “extra” package with a name like
+
To allow UxPlay to work with Bluetooth Low Energy (LE) Service
+Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous) service
+discovery, start it with the option
+“-ble <path-to-writeable-file>”, which at startup
+writes a data file containing the uxplay process ID and process name,
+and is deleted when uxplay terminates normally. This file is not
+used in the simple manual method for creating a beacon described
+below.
+Bluetooth LE Service discovery uses a “beacon” broadcasting a simple
+12-byte advertisement “0B FF 4C 00 09 06 03 30 XX XX XX XX”
+where XX XX XX XX is an IPv4 internet address of the UxPlay host
+translated into hexadecimal octets: For example,
+“XX XX XX XX” = “C0 A8 01 FD” means
+192.168.2.253. UxPlay must be able to receive messages on TCP port 7000
+at this address. The uxplay option “-p” sets up uxplay to
+listen on port 7000 for these messages.
+The full translation of this message is that it has length 0B = 0x0b
+= 11 octets, and is a single “Advertising Protocol Data Unit” (PDU) of
+type “FF”, called “Manufacturer-Specific Data”, with
+“manufacturer code” “4C 00” = 0x004c = Apple (note the
+reversal of octet order when two octets are combined to make a two-byte
+unsigned short integer), and “09 06 03 30 XX XX XX XX” is
+the Apple-specific data.
+The Apple-specific data contains a single Apple Data Unit with Apple
+type = 09 (Airplay), Apple Data length 06 (0x06 = 6 octets) and Apple
+Data “03 30 XX XX XX XX” where 03 = 0000 0011 is Apple
+Flags, 30 is a seed (which will be ignored), and XX XX XX XX is the IPv4
+internet address. This is smaller than the “iBeacon” Apple Data Unit,
+which has Apple type 02 and Apple length 15 (0x15 = 21 octets).
+In addition to creating the message, we need to set the “Advertising
+type” (ADV_NONCONN_IND) and “Advertising interval” range [AdvMin,
+AdvMax], where 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000
+= 10.24 sec (intervals are given in units of 0.625 msec as uint16_t
+unsigned short integers). Setting AdvMin = AdvMax fixes the interval;
+AdvMin < AdvMax allows the choice of the time of each advertising
+broadcast to be flexible within an allowed window to avoid clashing with
+other Bluetooth tasks. Keep the default choice to broadcast
+simultaneously on all three advertising channels, 37,38,39.
+An automated script to setup and start the beacon should use a
+high-level interface such as: (Linux) Bluez LEAdvertisingManager1
+(with an example)
+and (Windows 10/11) BluetoothLEAdvertisementPublisherClass
+(with an example).
+We invite submission of Pull Requests for working
+implementations!
+Until automated scripts are available, a simple Linux-only low-level
+manual method is given below, using the hcitool and
+hciconfig utilities which directly access the HCI stack,
+and need elevated privileges (use sudo). These utilities
+have been declared “deprecated” and “obsolete” by BlueZ developers: on
+Debian-based Linux “sudo apt install bluez” still provides
+hcitool, but on some other Linux distributions, it is split
+off from the main BlueZ package into an “extra” package with a name like
“bluez-deprecated”. If we get the AirPlay beacon to work using the newer
-bluetoothctl utility, these instructions will be
-updated.
-
-These manual instructions will hopefully be soon
-superseded by e.g. python scripts that automate beacon control, probably
-using D-Bus on Linux. Please submit any such scripts you get working for
-possible packaging together with UxPlay. Note that the Apple
-Service Discovery beacon is not a standard “ibeacon”,
-and cannot be set up with unmodified “ibeacon”-specific
-applications.
-For testing Bluetooth beacon Service Discovery on Linux, you will
-need to suppress the avahi-daemon which provides DNS-SD Service
-Discovery on UxPlay’s Host system (replace mask and
-stop below by unmask and start to
-restore DNS-SD service).;
-
-$ sudo systemctl mask avahi-daemon.socket
-$ sudo systemctl stop avahi-daemon
-
-Then verify that uxplay will not start without the
--ble <filename> option.
-Before starting, check that you have a Bluetooth device with
-“hcitool dev”
+bluetoothctl or btmgmt utilities, these
+instructions will be updated.
+First verify that a Bluetooth HCI interface is available:
$hcitool dev
Devices:
hci1 E8:EA:6A:7C:3F:CC
hci0 08:BE:AC:40:A9:DC
This shows two devices with their MAC addresses. You can use
-“hciconfig -i” to see which version of Bluetooth they
-implement: we require Bluetooth v4.0 or later. Choose which to use (we
-will use hci0), and reset it.
+“hciconfig -a” to see which versions of Bluetooth they
+implement: we require Bluetooth v4.0 or later; you may need to use a
+cheap USB Bluetooth dongle if your system does not have it, or will not
+let you use it for LE (Low Energy) transmissions.
+Choose which interface to use (we will use hci0), and reset it.
$ sudo hciconfig hci0 reset
-
-- Step 1. First reconfigure the Bluetooth device
-(hci0):
-
-hcitool sends HCI commands as a sequence of 1-byte
-hexadecimal octets. It echoes the length (here plen = 15
-bytes) and content of the sequence it sends to the Bluetooth HCI stack,
-and of the 4-byte “HCI Event” response it gets. Only the last byte of
-the response is important: 00 means the command succeded
-(other values are error codes).
+Step 1. Configure the beacon by sending a configure
+command 0x0006 to the Bluetooth LE stack 0x08. hcitool
+echoes the HCI command and the 4-byte “HCI Event” response. The only
+important part of the response is that the last byte is
+“00” (= “success”: other values are error codes):
-$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
+$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
< HCI Command: ogf 0x08, ocf 0x0006, plen 15
- A0 00 A0 00 03 01 00 00 00 00 00 00 00 07 00
+ A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00
> HCI Event: 0x0e plen 4
02 06 20 00
-The above command configures the beacon:
-“cmd 0x08 0x006” means HCI LE (ogf=0x08) command number 6
-(ocf=0x0006) of the Blutooth LE stack.
-The first two message bytes “0xa0 0x00” means a 2-byte
-“unsigned short” value 0x00a0. (uint16_t integers such as 0xabcd are
-represented as two bytes “0xcd, 0xab”). This is the minimum
-interval AdvMin between beacon broadcasts, which are essentially
-simultaneous on all three advertising channels. The next two entries
-represent the maximum interval AdvMax, also set to 0x00a0, which means
-100 msec (200 msec would be 2 * 0x00a0 = 0x0140 or
-“0x40 0x01”). Setting AdvMin = AdvMax fixes the interval
-between transmissions. If AdvMin < AdvMax, the timing of each
-broadcast event relative to the previous one can be chosen flexibly to
-not overlap with any other task the bluetooth socket is carrying out.
-The allowed range of these parameters is 0x00a0 = 100 msec <= AdvMin
-<= AdvMax <= 0x4000 = 10.24 sec.
-An Apple TV (Gen 3) seems to use a fixed interval of 180 msec =
-0x0120 (“0x20 0x01”).
-The sixth byte TxAdd = “0x01” says that a random MAC
-“advertisement address”” AdvAddr for the Bluetooth device will be sent
-with the advertisement. If you wish to send the true hardware MAC
-address of the Bluetooth device, replace this byte by
-“0x00”.
-These are the only parameters you might want to
-vary. The fifth byte 0x03 is the Advertising PDU type
-“ADV_NONCONN_IND” (a beacon that transmits without accepting
-connections) and the fourteenth byte 0x07 is a flag 0000 0111 that says
-to use all three Bluetooth LE advertising channels.
-
-- Step 2. (Optional: skip this if you changed
-byte 6 of the initial configuration message from
-“
0x01” to “0x00”.) Use HCI LE
-command 5 (ocf=0x0005) to set private “advertising address” AdvAddr,
-which substitutes for the public MAC address of the Bluetooth
-device.
-
-This uses six random bytes r1,..,r6 and enters them as
-r1 , r2 , r3, r4, r5, r0 = (r6 | 0x03), where the 6th
-byte has been masked with 0x03 = 00000011 so its last two bits are on,
-and the value r0 is restricted to 64 values “0xrs” where
-the second hexadecimal digit s is one of {3, 7, b, f},
-which indicates a “static random” private address that is guaranteed to
-not change between device reboots. Note that Apple TV’s use random
-private addresses without applying a mask to r6 to distinguish between
-different types.
-
-$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
-< HCI Command: ogf 0x08, ocf 0x0005, plen 6
- 52 AA AA 3A B4 2F
-> HCI Event: 0x0e plen 4
- 02 05 20 00
-On a Bluetooth packet sniffer with wireshark, this address displays
-as: Advertising Address: 2f:b4:3a:aa:aa:52
-
-- Step 3. Now provide the advertising message, with
-HCI LE command 8 (ocf=0x0008):
-
-This sends a 32 byte message to the HCI LE stack, where the first
-byte is the length (here 0x0c = 12 bytes) of the significant part of the
-following 31 bytes: 12 significant bytes, padded with 19 zeros to a
-total message length of 32 bytes. (hcitool requires a
-message padded to the full 32 bytes, but only sends the significant
-bytes to the Bluetooth LE stack.)
+The first “0xa0 0x00” sets AdvMin = 0x00a0 = 100 msec.
+The second “0xa0 0x00” sets AdvMax = 0x00a0 = 100 msec.
+Then “0x03” sets the Advertising Type to ADV_NONCONN_IND.
+The other non-zero entry (0x07 = 0000 0111) is the flag for using all
+three advertising channels.
+An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120
+(“0x20 0x01”).
+Step 2. Set the advertising message with HCI LE
+command 0x0008. For this command, hcitool requires a 32 octet message
+after sudo hcitool -i hci0 cmd 0x08 0x0008: The first octet
+is the length 0C = 0x0c = 12 of the “significant part” of the following
+31 octets, followed by the 12 octets of the advertisement, then padded
+with 19 zeroes to a total length of 32 octets. The example below sends
+an IPv4 address 192.168.1.253 as “0xc0 0xa8 0x01 0xfd”:
$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
< HCI Command: ogf 0x08, ocf 0x0008, plen 32
0C 0B FF 4C 00 09 06 03 30 C0 A8 01 FD 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
> HCI Event: 0x0e plen 4
01 08 20 00
-The only parts of this message that you must change are the four
-bytes 10,11,12,13, of the IPv4 address, here
-“0xc0 0xa8 0x01 0xfd”, (decimal 192 168 1 253, an IPv4
-address 192.168.1.253) which should be an IPv4 address at which the
-UxPlay server can receive requests from iOS/macOS clients at TCP port
-7000. You need to find what IPv4 address will work on the computer that
-hosts UxPlay (use ifconfig), convert each of the four
-numbers from decimal to hexadecimal, and replace bytes 13-16 of the
-message by them.
-
-- Step 4. Start advertising by the beacon with
-Bluetooth LE command 10 (ocf = 0x000a) and 1-byte message
-“
0x01” = “on”.
-
+Step 3. Start the beacon with a 1-byte message
+“0x01” = “on”, sent with HCI LE command 0x000a = 10:
$ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
< HCI Command: ogf 0x08, ocf 0x000a, plen 1
01
> HCI Event: 0x0e plen 4
02 0A 20 00
-(To stop advertising, use this command to send the 1-byte message
-“0x00” = “off”.)
-For creating a higher-level script, it might be useful to know that
-the length 0C = 12 bytes advertisement sent in step 3 has a single
-“Advertising Protocol Data Unit” (PDU):
+The full length of the broadcasted beacon message is 43 bytes. To
+stop the beacon, use this command to send the 1-byte message
+“0x00” = “off”.
-- 0B FF 4C 00 09 06 03 30 C0 A8 01 FD: length 0B = 11 bytes,
-consisting of: FF ( type = manufacturer-specific) 4C 00 (manufacturer
-code = 0x004c, Apple ) manufacturer data 09 06 03 30 C0 A8 01 FD
+- For testing Bluetooth beacon Service Discovery on Linux, you will
+need to suppress the avahi-daemon which provides DNS-SD Service
+Discovery on UxPlay’s Host system (replace
mask and
+stop below by unmask and start to
+restore DNS-SD service):
+
+$ sudo systemctl mask avahi-daemon.socket
+$ sudo systemctl stop avahi-daemon
+An automated procedure for creating the beacon would presumably want
+to switch it on when uxplay starts, and off when it stops. The 20-byte
+file created when uxplay starts (and deleted when it stops) contains the
+PID as a uint32_t unsigned integer in the first 4 bytes, followed by up
+to the first 11 characters of the process name (usually “uxplay”) as a
+null-terminated string, padded with zeroes to 16 bytes. This data can be
+used to test whether uxplay is actually running, including cases where
+it has segfaulted and not deleted the file.
+This method above creates a beacon that identifies itself with a
+“public Advertising Address” (the MAC hardware address of the Bluetooth
+device). An Apple TV uses a private random address. If you wish to do
+that, change the sixth octet (the one following 0x03) in
+Step 1 from “TxAdd” = 0x00 to TxAdd = 0x01,
+and add an intermediate “step 1.5”:
+Step 1.5 Choose 6 random bytes r1, r2, r3, r4, r5,
+r6, such as “0x52 0xaa, 0xaa, 0x3a, 0xb4, 0x2f”, and use
+HCI LE command 0x0005 to set the random address:
+$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
+< HCI Command: ogf 0x08, ocf 0x0005, plen 6
+ 52 AA AA 3A B4 2F
+> HCI Event: 0x0e plen 4
+ 02 05 20 00
+On a Bluetooth packet sniffer with wireshark, this address displays
+as: Advertising Address: 2f:b4:3a:aa:aa:52. In
+principle, random byte r6 should be masked with 0x03 (r6 = r6 | 0x03) to
+mark the address as a “static random private address”, but Apple TV does
+not do this. In fact it updates to a new random Advertising Address
+every 20 mins or so, increasing the seed in the Apple Data by 1 each
+time. Apple TV’s also add a length 2 type 0x01 (“Flags”) Advertising PDU
+“0x02 0x01 0x1a” in front of the main type 0xff
+“Manufacturer-Specific Data” Advertising PDU in Step 2. This is
+“optional” for ADV_NONCONN_IND advertisement type, and testing shows
+that it can be dropped without affecting Service Discovery, which is
+fortunate because the high-level Linux and Windows interfaces mentioned
+earlier do not permit users to send a “Flags”-type PDU.
+
+- Our current understanding is that Bluetooth LE AirPlay
+Service Discovery only supports broadcast of IPv4 addresses. Please let
+us know if this is incorrect, or if IPv6 support is introduced in the
+future.
-The manufacturer data defined by Apple consists of a single Apple
-data unit: 09 (Apple type = AirPlay), 06 (Apple data length 6 bytes)
-Apple data 03 30 XX XX XX XX, broken down into 03 (flags: 0000 0011) 30
-(a seed) XX XX XX XX (IPv4 network address, written as four hexadecimal
-octets in standard order). (Apple TV’s use a random private “AdvAddr”
-address as described above, and periodically update it at about 20 min
-intervals, each time increasing the seed by 1.)
-Apple TV’s also insert a type-1 (“Flags”) 2-byte PDU
-“02 01 1A” before the manufacturer-specific PDU, increasing
-the significant length of the message to 0xf = 15 bytes. It turns out
-that the “Flags” PDU is “optional” for advertisements like beacons that
-do not allow client connections: in our tests on v4.0 and later dongles,
-Service Discovery still worked fine after dropping the “Flags” PDU.
-Both Linux and Windows have high-level interfaces that support users
-sending Advertising PDU’s, but restricted to type 0xff
-“manufacturer-specific-data” only, without any “Flags”. These should be
-used for automating beacon setup, and are: (Linux) Bluez LEAdvertisingManager1
-and (Windows 10/11) BluetoothLEAdvertisementPublisherClass
-(with an example).
-We don’t know if these instructions can be modified to
-advertise IPv6 addresses: if you know of any verified support for
-Bluetooth LE IPv6 Service Discovery in newer AppleTV models, please let
-us know. Simply replacing the 4-byte IPv4 address with a 16-byte IPv6
-address (and adjusting the lengths at bytes 1, 5 and 10) does not seem
-to work, although perhaps we did not find the right value for byte 11
-(“Apple Flags”). If Apple’s Bluetooth LE Service Discovery has IPv6
-support, we need to examine the beacon advertisement packet for IPv6
-addresses with a Bluetooth sniffer.
Troubleshooting
Note: uxplay is run from a terminal command line, and
informational messages are written to the terminal.
diff --git a/README.md b/README.md
index a556426..9208d5b 100644
--- a/README.md
+++ b/README.md
@@ -1458,42 +1458,49 @@ GStreamer inner workings.
# Bluetooth LE beacon setup
-When uxplay is started with the option `uxplay -ble `, it writes a 20 byte data file
-containing (4 bytes) the process ID (PID) as a uint32_t 32-bit unsigned integer, and
-(16 bytes) up to 15 bytes of the process name (usually "uxplay") as a null-terminated string,
-padded with zeroes to fill 16 bytes. The file is deleted if UxPlay is terminated normally (without a segfault),
-and could be used to determine if an instance of uxplay is running. **This file is provided for possible future use
-in a script for controlling the beacon, and will not be used here**.
-
-You may need to use a cheap USB Bluetooth dongle if your system does not have Bluetooth 4.0 or later,
-or will not let you use it for LE (Low Energy) transmissions.
+To allow UxPlay to work with Bluetooth Low Energy (LE) Service Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous)
+service discovery, start it with the option "`-ble `", which at startup writes a data file containing
+the uxplay process ID and process name, and is deleted when uxplay terminates normally. **This file
+is not used in the simple manual method for creating a beacon described below**.
-These instructions are tested on Linux using the Bluez Bluetooth stack. They use the `hcitool` and ``hciconfig``
+Bluetooth LE Service discovery uses a "beacon" broadcasting a simple 12-byte
+advertisement "`0B FF 4C 00 09 06 03 30 XX XX XX XX`" where XX XX XX XX is an IPv4 internet
+address of the UxPlay host translated into hexadecimal octets: For
+example, "`XX XX XX XX`" = "``C0 A8 01 FD``" means 192.168.2.253. UxPlay
+must be able to receive messages on TCP port 7000 at this
+address. The uxplay option "`-p`" sets up uxplay to listen on port 7000 for these messages.
+
+The full translation of this message is that it has length 0B = 0x0b = 11 octets, and is a single "Advertising Protocol Data Unit" (PDU) of type "`FF`",
+called "Manufacturer-Specific Data", with "manufacturer code" "`4C 00`" = 0x004c = Apple (note the reversal of octet order when
+two octets are combined to make a two-byte unsigned short integer), and "`09 06 03 30 XX XX XX XX`" is the Apple-specific data.
+
+The Apple-specific data contains a single Apple Data Unit with Apple type = 09 (Airplay), Apple Data length 06 (0x06 = 6 octets) and
+Apple Data "`03 30 XX XX XX XX`" where 03 = 0000 0011 is Apple Flags, 30 is a seed (which will be ignored), and XX XX XX XX
+is the IPv4 internet address. This is smaller than the "iBeacon" Apple Data Unit, which has Apple type 02 and Apple length 15 (0x15 = 21 octets).
+
+In addition to creating the message, we need to set the "Advertising type" (ADV_NONCONN_IND) and "Advertising interval" range [AdvMin, AdvMax],
+where 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000 = 10.24 sec
+(intervals are given in units of 0.625 msec as uint16_t unsigned short integers). Setting AdvMin = AdvMax fixes the interval; AdvMin < AdvMax allows the choice
+of the time of each advertising broadcast to be flexible within an allowed window to avoid clashing with other Bluetooth tasks.
+Keep the default choice to broadcast simultaneously on all three advertising channels, 37,38,39.
+
+An automated script to setup and start the beacon should use a high-level interface
+such as: (Linux) Bluez [LEAdvertisingManager1](https://manpages.opensuse.org/Leap-16.0/bluez/org.bluez.LEAdvertisement.5.en.html) (with
+an [example](https://github.com/bluez/bluez/blob/master/test/example-advertisement))
+and (Windows 10/11) [BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
+(with an [example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
+**We invite submission of Pull Requests for working implementations!**
+
+Until automated scripts are available, a simple Linux-only low-level manual method
+is given below, using the `hcitool` and ``hciconfig``
utilities which directly access the HCI stack, and need elevated privileges (use `sudo`). These utilities
-have been declared "deprecated" and "obsolete" by BlueZ developers: on Debian-based Linux `sudo apt install bluez`
+have been declared "deprecated" and "obsolete" by BlueZ developers: on Debian-based Linux "`sudo apt install bluez`"
still provides `hcitool`, but on some other Linux distributions, it is split off from the main BlueZ package into an "extra" package
-with a name like "bluez-deprecated". If we get the AirPlay beacon to work using the newer `bluetoothctl` utility, these instructions will be
-updated.
+with a name like "bluez-deprecated". If we get the AirPlay beacon to work using the newer `bluetoothctl` or ``btmgmt`` utilities,
+these instructions will be updated.
-* **These manual instructions will hopefully be soon superseded by e.g. python scripts that automate beacon control, probably using D-Bus on Linux. Please
- submit any such scripts you get working for possible packaging together with UxPlay**. Note that the Apple Service Discovery beacon is
- not a standard "**ibeacon**", and cannot be set up with unmodified "ibeacon"-specific applications.
-
-* For testing Bluetooth beacon Service Discovery on Linux, you will need to suppress the avahi-daemon which
-provides DNS-SD Service Discovery on UxPlay's Host system (replace `mask` and ``stop``
-below by `unmask` and ``start`` to restore DNS-SD service).;
-
-```
-$ sudo systemctl mask avahi-daemon.socket
-$ sudo systemctl stop avahi-daemon
-
-```
-Then verify that uxplay will not start without the `-ble ` option.
-
-
-
-Before starting, check that you have a Bluetooth device with "`hcitool dev`"
+First verify that a Bluetooth HCI interface is available:
```
$hcitool dev
@@ -1502,80 +1509,44 @@ Devices:
hci0 08:BE:AC:40:A9:DC
```
-This shows two devices with their MAC addresses. You can use "`hciconfig -i`" to see which version of Bluetooth they
-implement: we require Bluetooth v4.0 or later. Choose which to use (we will use hci0), and reset it.
-
+This shows two devices with their MAC addresses. You can use "`hciconfig -a`" to see which versions of Bluetooth they
+implement: we require Bluetooth v4.0 or later;
+you may need to use a cheap USB Bluetooth dongle if your system does not have it,
+or will not let you use it for LE (Low Energy) transmissions.
+Choose which interface to use (we will use hci0), and reset it.
+
```
$ sudo hciconfig hci0 reset
```
-* **Step 1.** First reconfigure the Bluetooth device (hci0):
+**Step 1.** Configure the beacon by sending a configure command 0x0006 to the Bluetooth LE stack 0x08. `hcitool` echoes the HCI command
+and the 4-byte "HCI Event" response. The only important part of the response is that the last byte is "`00`" (= "success":
+other values are error codes):
-`hcitool` sends HCI commands as a sequence of 1-byte hexadecimal octets. It echoes the length (here `plen` = 15 bytes) and content
-of the sequence it sends to the Bluetooth HCI stack, and of the 4-byte "HCI Event" response it gets.
-Only the last byte of the response is important: `00` means the command succeded (other values are error codes).
```
-$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
+$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
< HCI Command: ogf 0x08, ocf 0x0006, plen 15
- A0 00 A0 00 03 01 00 00 00 00 00 00 00 07 00
+ A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00
> HCI Event: 0x0e plen 4
02 06 20 00
```
-The above command configures the beacon: "`cmd 0x08 0x006`" means HCI LE (ogf=0x08) command number 6 (ocf=0x0006) of
-the Blutooth LE stack.
+The first "`0xa0 0x00`" sets AdvMin = 0x00a0 = 100 msec. The second "``0xa0 0x00``" sets AdvMax = 0x00a0 = 100 msec.
+Then "`0x03`" sets the Advertising Type to ADV_NONCONN_IND. The other non-zero entry (0x07 = 0000 0111) is the flag for using
+all three
+advertising channels.
-The first two message bytes "`0xa0 0x00`" means a 2-byte "unsigned short" value 0x00a0. (uint16_t integers such as 0xabcd are represented as two bytes "`0xcd, 0xab`"). This is the minimum interval AdvMin between
-beacon broadcasts, which are essentially simultaneous on all three advertising channels. The next two entries represent the maximum interval AdvMax, also set to 0x00a0,
-which means 100 msec (200 msec would be 2 * 0x00a0 = 0x0140 or "`0x40 0x01`"). Setting AdvMin = AdvMax fixes the interval between transmissions.
-If AdvMin < AdvMax, the timing of each broadcast event relative to the previous one can be chosen flexibly to not overlap with any other task the bluetooth socket is carrying out.
-The allowed range of these parameters is 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000 = 10.24 sec.
+An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120 ("`0x20 0x01`").
-An Apple TV (Gen 3) seems to use a fixed interval of 180 msec = 0x0120 ("`0x20 0x01`").
-
-The sixth byte TxAdd = "`0x01`" says that a random MAC "advertisement address"" AdvAddr for the Bluetooth device will be sent with the advertisement. If you wish to send the true
-hardware MAC address of the Bluetooth device, replace this byte by "`0x00`".
-
-
-__These are the only parameters you might want to vary__. The fifth byte 0x03 is the Advertising PDU type "ADV_NONCONN_IND" (a beacon that transmits without accepting connections)
-and the fourteenth byte 0x07 is a flag 0000 0111 that says to use all three Bluetooth LE advertising channels.
-
-
-
-* **Step 2.** (**Optional: skip this if you changed byte 6 of the initial configuration message from** "``0x01``" **to** "`0x00`".)
-Use HCI LE command 5 (ocf=0x0005) to
-set private "advertising address" AdvAddr, which substitutes for the public MAC address of the Bluetooth device.
-
-This uses six random bytes r1,..,r6 and enters them as
-` r1 , r2 , r3, r4, r5, r0 = (r6 | 0x03)`, where the 6th byte has been masked with 0x03 = 00000011 so its last two bits are on,
-and the value r0 is restricted to 64 values "`0xrs`" where the second
-hexadecimal digit ``s`` is one of {3, 7, b, f}, which indicates a "static random" private address that is guaranteed to not
-change between device reboots. Note that Apple TV's use random private addresses without applying a mask to r6 to distinguish
-between different types.
-
-
-```
-
-$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
-< HCI Command: ogf 0x08, ocf 0x0005, plen 6
- 52 AA AA 3A B4 2F
-> HCI Event: 0x0e plen 4
- 02 05 20 00
-```
-
-On a Bluetooth packet sniffer with wireshark, this address displays as: **Advertising Address: 2f:b4:3a:aa:aa:52**
-
-* **Step 3.** Now provide the advertising message, with HCI LE command 8 (ocf=0x0008):
-
-
-This sends a 32 byte message to the HCI LE stack, where the first byte is the length (here 0x0c = 12 bytes) of the significant part of
-the following 31 bytes: 12 significant bytes, padded with 19 zeros to a total message length of 32 bytes. (`hcitool` requires a message padded to the full
-32 bytes, but only sends the significant bytes to the Bluetooth LE stack.)
+**Step 2.** Set the advertising message with HCI LE command 0x0008. For this command, hcitool requires a 32 octet message after
+`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0C = 0x0c = 12 of the "significant part" of the following 31 octets,
+followed by the 12 octets of the advertisement, then padded with 19 zeroes to a total length of 32 octets. The example below sends an
+IPv4 address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`":
```
$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
@@ -1586,12 +1557,7 @@ $ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0
01 08 20 00
```
-The only parts of this message that you must change are the four bytes 10,11,12,13, of the IPv4 address, here "` 0xc0 0xa8 0x01 0xfd `", (decimal 192 168 1 253, an IPv4 address 192.168.1.253)
- which should be an IPv4 address at which the UxPlay server can receive requests from iOS/macOS clients at TCP port 7000. You need to find what IPv4 address will work
-on the computer that hosts UxPlay (use `ifconfig`), convert each of the four numbers from decimal to hexadecimal, and replace bytes 13-16 of the message by them.
-
-
-* **Step 4.** Start advertising by the beacon with Bluetooth LE command 10 (ocf = 0x000a) and 1-byte message "`0x01`" = "on".
+**Step 3**. Start the beacon with a 1-byte message "`0x01`" = "on", sent with HCI LE command 0x000a = 10:
```
$ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
@@ -1600,35 +1566,56 @@ $ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
> HCI Event: 0x0e plen 4
02 0A 20 00
```
-
-(To stop advertising, use this command to send the 1-byte message "`0x00`" = "off".)
+The full length of the broadcasted beacon message is 43 bytes.
+To stop the beacon, use this command to send the 1-byte message "`0x00`" = "off".
-For creating a higher-level script, it might be useful to know that the length 0C = 12 bytes advertisement sent in step 3 has a single "Advertising Protocol Data Unit" (PDU):
+* For testing Bluetooth beacon Service Discovery on Linux, you will need to suppress the avahi-daemon which
+provides DNS-SD Service Discovery on UxPlay's Host system (replace `mask` and ``stop`` below
+by `unmask` and ``start`` to restore DNS-SD service):
-* 0B FF 4C 00 09 06 03 30 C0 A8 01 FD: length 0B = 11 bytes, consisting of: FF ( type = manufacturer-specific) 4C 00 (manufacturer code = 0x004c, Apple ) manufacturer data 09 06 03 30 C0 A8 01 FD
-
-The manufacturer data defined by Apple consists of a single Apple data unit: 09 (Apple type = AirPlay), 06 (Apple data length 6 bytes) Apple data 03 30 XX XX XX XX, broken down into 03 (flags: 0000 0011) 30 (a seed)
-XX XX XX XX (IPv4 network address, written as four hexadecimal octets in standard order). (Apple TV's use a random private "AdvAddr" address as described above, and periodically update it at about 20 min intervals, each time
-increasing the seed by 1.)
-
-Apple TV's also insert a type-1 ("Flags") 2-byte PDU "`02 01 1A`" before the manufacturer-specific PDU, increasing the significant length of the message to 0xf = 15 bytes.
-It turns out that the "Flags" PDU is "optional" for advertisements like beacons that do not allow client connections:
-in our tests on v4.0 and later dongles, Service Discovery still worked fine after dropping the "Flags" PDU.
-
-Both Linux and Windows have high-level interfaces that support users sending Advertising PDU's, but restricted to
-type 0xff "manufacturer-specific-data" only, without any "Flags". These should be used for automating beacon setup, and
-are: (Linux) Bluez [LEAdvertisingManager1](https://github.com/bluez/bluez/blob/master/test/example-advertisement)
-and (Windows 10/11) [BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
-(with an [example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
+```
+$ sudo systemctl mask avahi-daemon.socket
+$ sudo systemctl stop avahi-daemon
+```
-**We don't know if these instructions can be modified to advertise IPv6 addresses: if you know of any verified support for Bluetooth LE IPv6 Service Discovery in newer AppleTV models,
-please let us know. Simply replacing the 4-byte IPv4 address with a 16-byte IPv6 address (and adjusting the lengths at bytes 1, 5 and 10) does not seem to work, although perhaps
-we did not find the right value for byte 11 ("Apple Flags"). If Apple's Bluetooth LE Service Discovery has IPv6 support, we need to examine the beacon advertisement packet
-for IPv6 addresses with a Bluetooth sniffer.**
+An automated procedure for creating the beacon would presumably want to switch it on when uxplay starts, and off when it
+stops. The 20-byte file created when uxplay starts (and deleted when it stops) contains the PID as a uint32_t unsigned integer in the first 4 bytes,
+followed by up to the first
+11 characters of the process name (usually "uxplay") as a null-terminated string, padded with zeroes to 16 bytes. This data can be used to test
+whether uxplay is actually running, including cases where it has segfaulted and not deleted the file.
+
+
+
+This method above creates a beacon that identifies itself with a "public Advertising Address" (the MAC hardware address of
+the Bluetooth device). An Apple TV uses a private random address. If you wish to do that, change the sixth octet (the one following `0x03`)
+in Step 1 from "TxAdd" = `0x00` to TxAdd = ``0x01``, and add an intermediate "step 1.5":
+
+**Step 1.5** Choose 6 random bytes r1, r2, r3, r4, r5, r6, such as
+"`0x52 0xaa, 0xaa, 0x3a, 0xb4, 0x2f`", and use HCI LE command 0x0005 to set the random address:
+
+```
+$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
+< HCI Command: ogf 0x08, ocf 0x0005, plen 6
+ 52 AA AA 3A B4 2F
+> HCI Event: 0x0e plen 4
+ 02 05 20 00
+```
+
+On a Bluetooth packet sniffer with wireshark, this address displays as: **Advertising Address: 2f:b4:3a:aa:aa:52**.
+In principle, random byte r6 should be masked with 0x03 (r6 = r6 | 0x03) to mark the address as a "static random private address",
+but Apple TV does not do this. In fact it updates to a new random Advertising Address every 20 mins or so, increasing
+the seed in the Apple Data by 1 each time. Apple TV's also add a length 2 type 0x01 ("Flags") Advertising PDU "`0x02 0x01 0x1a`" in front of
+the main type 0xff "Manufacturer-Specific Data" Advertising PDU in Step 2. This is "optional" for ADV_NONCONN_IND advertisement type,
+and testing shows that it can be dropped without affecting Service Discovery, which is fortunate
+because the high-level Linux and Windows interfaces mentioned earlier do not permit users to send a "Flags"-type PDU.
+
+
+* **Our current understanding is that Bluetooth LE AirPlay Service Discovery only supports
+broadcast of IPv4 addresses. Please let us know if this is incorrect, or if IPv6 support is introduced in the future.**
# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
diff --git a/README.txt b/README.txt
index be5a657..3e0017d 100644
--- a/README.txt
+++ b/README.txt
@@ -1481,53 +1481,69 @@ this to see even more of the GStreamer inner workings.
# Bluetooth LE beacon setup
-When uxplay is started with the option
-`uxplay -ble `, it writes a 20 byte data file
-containing (4 bytes) the process ID (PID) as a uint32_t 32-bit unsigned
-integer, and (16 bytes) up to 15 bytes of the process name (usually
-"uxplay") as a null-terminated string, padded with zeroes to fill 16
-bytes. The file is deleted if UxPlay is terminated normally (without a
-segfault), and could be used to determine if an instance of uxplay is
-running. **This file is provided for possible future use in a script for
-controlling the beacon, and will not be used here**.
+To allow UxPlay to work with Bluetooth Low Energy (LE) Service
+Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous) service
+discovery, start it with the option "`-ble `",
+which at startup writes a data file containing the uxplay process ID and
+process name, and is deleted when uxplay terminates normally. **This
+file is not used in the simple manual method for creating a beacon
+described below**.
-You may need to use a cheap USB Bluetooth dongle if your system does not
-have Bluetooth 4.0 or later, or will not let you use it for LE (Low
-Energy) transmissions.
+Bluetooth LE Service discovery uses a "beacon" broadcasting a simple
+12-byte advertisement "`0B FF 4C 00 09 06 03 30 XX XX XX XX`" where XX
+XX XX XX is an IPv4 internet address of the UxPlay host translated into
+hexadecimal octets: For example, "`XX XX XX XX`" = "`C0 A8 01 FD`" means
+192.168.2.253. UxPlay must be able to receive messages on TCP port 7000
+at this address. The uxplay option "`-p`" sets up uxplay to listen on
+port 7000 for these messages.
-These instructions are tested on Linux using the Bluez Bluetooth stack.
-They use the `hcitool` and `hciconfig` utilities which directly access
-the HCI stack, and need elevated privileges (use `sudo`). These
-utilities have been declared "deprecated" and "obsolete" by BlueZ
-developers: on Debian-based Linux `sudo apt install bluez` still
-provides `hcitool`, but on some other Linux distributions, it is split
-off from the main BlueZ package into an "extra" package with a name like
-"bluez-deprecated". If we get the AirPlay beacon to work using the newer
-`bluetoothctl` utility, these instructions will be updated.
+The full translation of this message is that it has length 0B = 0x0b =
+11 octets, and is a single "Advertising Protocol Data Unit" (PDU) of
+type "`FF`", called "Manufacturer-Specific Data", with "manufacturer
+code" "`4C 00`" = 0x004c = Apple (note the reversal of octet order when
+two octets are combined to make a two-byte unsigned short integer), and
+"`09 06 03 30 XX XX XX XX`" is the Apple-specific data.
-- **These manual instructions will hopefully be soon superseded by
- e.g. python scripts that automate beacon control, probably using
- D-Bus on Linux. Please submit any such scripts you get working for
- possible packaging together with UxPlay**. Note that the Apple
- Service Discovery beacon is not a standard "**ibeacon**", and cannot
- be set up with unmodified "ibeacon"-specific applications.
+The Apple-specific data contains a single Apple Data Unit with Apple
+type = 09 (Airplay), Apple Data length 06 (0x06 = 6 octets) and Apple
+Data "`03 30 XX XX XX XX`" where 03 = 0000 0011 is Apple Flags, 30 is a
+seed (which will be ignored), and XX XX XX XX is the IPv4 internet
+address. This is smaller than the "iBeacon" Apple Data Unit, which has
+Apple type 02 and Apple length 15 (0x15 = 21 octets).
-- For testing Bluetooth beacon Service Discovery on Linux, you will
- need to suppress the avahi-daemon which provides DNS-SD Service
- Discovery on UxPlay's Host system (replace `mask` and `stop` below
- by `unmask` and `start` to restore DNS-SD service).;
+In addition to creating the message, we need to set the "Advertising
+type" (ADV_NONCONN_IND) and "Advertising interval" range \[AdvMin,
+AdvMax\], where 0x00a0 = 100 msec \<= AdvMin \<= AdvMax \<= 0x4000 =
+10.24 sec (intervals are given in units of 0.625 msec as uint16_t
+unsigned short integers). Setting AdvMin = AdvMax fixes the interval;
+AdvMin \< AdvMax allows the choice of the time of each advertising
+broadcast to be flexible within an allowed window to avoid clashing with
+other Bluetooth tasks. Keep the default choice to broadcast
+simultaneously on all three advertising channels, 37,38,39.
-```{=html}
-
-```
- $ sudo systemctl mask avahi-daemon.socket
- $ sudo systemctl stop avahi-daemon
+An automated script to setup and start the beacon should use a
+high-level interface such as: (Linux) Bluez
+[LEAdvertisingManager1](https://manpages.opensuse.org/Leap-16.0/bluez/org.bluez.LEAdvertisement.5.en.html)
+(with an
+[example](https://github.com/bluez/bluez/blob/master/test/example-advertisement))
+and (Windows 10/11)
+[BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
+(with an
+[example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
+**We invite submission of Pull Requests for working implementations!**
-Then verify that uxplay will not start without the `-ble `
-option.
+Until automated scripts are available, a simple Linux-only low-level
+manual method is given below, using the `hcitool` and `hciconfig`
+utilities which directly access the HCI stack, and need elevated
+privileges (use `sudo`). These utilities have been declared "deprecated"
+and "obsolete" by BlueZ developers: on Debian-based Linux
+"`sudo apt install bluez`" still provides `hcitool`, but on some other
+Linux distributions, it is split off from the main BlueZ package into an
+"extra" package with a name like "bluez-deprecated". If we get the
+AirPlay beacon to work using the newer `bluetoothctl` or `btmgmt`
+utilities, these instructions will be updated.
-Before starting, check that you have a Bluetooth device with
-"`hcitool dev`"
+First verify that a Bluetooth HCI interface is available:
$hcitool dev
Devices:
@@ -1535,93 +1551,43 @@ Before starting, check that you have a Bluetooth device with
hci0 08:BE:AC:40:A9:DC
This shows two devices with their MAC addresses. You can use
-"`hciconfig -i`" to see which version of Bluetooth they implement: we
-require Bluetooth v4.0 or later. Choose which to use (we will use hci0),
-and reset it.
+"`hciconfig -a`" to see which versions of Bluetooth they implement: we
+require Bluetooth v4.0 or later; you may need to use a cheap USB
+Bluetooth dongle if your system does not have it, or will not let you
+use it for LE (Low Energy) transmissions.\
+Choose which interface to use (we will use hci0), and reset it.
$ sudo hciconfig hci0 reset
-- **Step 1.** First reconfigure the Bluetooth device (hci0):
-
-`hcitool` sends HCI commands as a sequence of 1-byte hexadecimal octets.
-It echoes the length (here `plen` = 15 bytes) and content of the
-sequence it sends to the Bluetooth HCI stack, and of the 4-byte "HCI
-Event" response it gets. Only the last byte of the response is
-important: `00` means the command succeded (other values are error
-codes).
+**Step 1.** Configure the beacon by sending a configure command 0x0006
+to the Bluetooth LE stack 0x08. `hcitool` echoes the HCI command and the
+4-byte "HCI Event" response. The only important part of the response is
+that the last byte is "`00`" (= "success": other values are error
+codes):
- $ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
+ $ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
< HCI Command: ogf 0x08, ocf 0x0006, plen 15
- A0 00 A0 00 03 01 00 00 00 00 00 00 00 07 00
+ A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00
> HCI Event: 0x0e plen 4
02 06 20 00
-The above command configures the beacon: "`cmd 0x08 0x006`" means HCI LE
-(ogf=0x08) command number 6 (ocf=0x0006) of the Blutooth LE stack.
+The first "`0xa0 0x00`" sets AdvMin = 0x00a0 = 100 msec. The second
+"`0xa0 0x00`" sets AdvMax = 0x00a0 = 100 msec. Then "`0x03`" sets the
+Advertising Type to ADV_NONCONN_IND. The other non-zero entry (0x07 =
+0000 0111) is the flag for using all three advertising channels.
-The first two message bytes "`0xa0 0x00`" means a 2-byte "unsigned
-short" value 0x00a0. (uint16_t integers such as 0xabcd are represented
-as two bytes "`0xcd, 0xab`"). This is the minimum interval AdvMin
-between beacon broadcasts, which are essentially simultaneous on all
-three advertising channels. The next two entries represent the maximum
-interval AdvMax, also set to 0x00a0, which means 100 msec (200 msec
-would be 2 \* 0x00a0 = 0x0140 or "`0x40 0x01`"). Setting AdvMin = AdvMax
-fixes the interval between transmissions. If AdvMin \< AdvMax, the
-timing of each broadcast event relative to the previous one can be
-chosen flexibly to not overlap with any other task the bluetooth socket
-is carrying out. The allowed range of these parameters is 0x00a0 = 100
-msec \<= AdvMin \<= AdvMax \<= 0x4000 = 10.24 sec.
-
-An Apple TV (Gen 3) seems to use a fixed interval of 180 msec = 0x0120
+An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120
("`0x20 0x01`").
-The sixth byte TxAdd = "`0x01`" says that a random MAC "advertisement
-address"" AdvAddr for the Bluetooth device will be sent with the
-advertisement. If you wish to send the true hardware MAC address of the
-Bluetooth device, replace this byte by "`0x00`".
-
-**These are the only parameters you might want to vary**. The fifth byte
-0x03 is the Advertising PDU type "ADV_NONCONN_IND" (a beacon that
-transmits without accepting connections) and the fourteenth byte 0x07 is
-a flag 0000 0111 that says to use all three Bluetooth LE advertising
-channels.
-
-- **Step 2.** (**Optional: skip this if you changed byte 6 of the
- initial configuration message from** "`0x01`" **to** "`0x00`".) Use
- HCI LE command 5 (ocf=0x0005) to set private "advertising address"
- AdvAddr, which substitutes for the public MAC address of the
- Bluetooth device.
-
-This uses six random bytes r1,..,r6 and enters them as
-`r1 , r2 , r3, r4, r5, r0 = (r6 | 0x03)`, where the 6th byte has been
-masked with 0x03 = 00000011 so its last two bits are on, and the value
-r0 is restricted to 64 values "`0xrs`" where the second hexadecimal
-digit `s` is one of {3, 7, b, f}, which indicates a "static random"
-private address that is guaranteed to not change between device reboots.
-Note that Apple TV's use random private addresses without applying a
-mask to r6 to distinguish between different types.
-
-
- $sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
- < HCI Command: ogf 0x08, ocf 0x0005, plen 6
- 52 AA AA 3A B4 2F
- > HCI Event: 0x0e plen 4
- 02 05 20 00
-
-On a Bluetooth packet sniffer with wireshark, this address displays as:
-**Advertising Address: 2f:b4:3a:aa:aa:52**
-
-- **Step 3.** Now provide the advertising message, with HCI LE command
- 8 (ocf=0x0008):
-
-This sends a 32 byte message to the HCI LE stack, where the first byte
-is the length (here 0x0c = 12 bytes) of the significant part of the
-following 31 bytes: 12 significant bytes, padded with 19 zeros to a
-total message length of 32 bytes. (`hcitool` requires a message padded
-to the full 32 bytes, but only sends the significant bytes to the
-Bluetooth LE stack.)
+**Step 2.** Set the advertising message with HCI LE command 0x0008. For
+this command, hcitool requires a 32 octet message after
+`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0C
+= 0x0c = 12 of the "significant part" of the following 31 octets,
+followed by the 12 octets of the advertisement, then padded with 19
+zeroes to a total length of 32 octets. The example below sends an IPv4
+address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`":
$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
< HCI Command: ogf 0x08, ocf 0x0008, plen 32
@@ -1630,73 +1596,73 @@ Bluetooth LE stack.)
> HCI Event: 0x0e plen 4
01 08 20 00
-The only parts of this message that you must change are the four bytes
-10,11,12,13, of the IPv4 address, here "`0xc0 0xa8 0x01 0xfd`", (decimal
-192 168 1 253, an IPv4 address 192.168.1.253) which should be an IPv4
-address at which the UxPlay server can receive requests from iOS/macOS
-clients at TCP port 7000. You need to find what IPv4 address will work
-on the computer that hosts UxPlay (use `ifconfig`), convert each of the
-four numbers from decimal to hexadecimal, and replace bytes 13-16 of the
-message by them.
+**Step 3**. Start the beacon with a 1-byte message "`0x01`" = "on", sent
+with HCI LE command 0x000a = 10:
-- **Step 4.** Start advertising by the beacon with Bluetooth LE
- command 10 (ocf = 0x000a) and 1-byte message "`0x01`" = "on".
-
-```{=html}
-
-```
$ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
< HCI Command: ogf 0x08, ocf 0x000a, plen 1
01
> HCI Event: 0x0e plen 4
02 0A 20 00
-(To stop advertising, use this command to send the 1-byte message
-"`0x00`" = "off".)
+The full length of the broadcasted beacon message is 43 bytes. To stop
+the beacon, use this command to send the 1-byte message "`0x00`" =
+"off".
-For creating a higher-level script, it might be useful to know that the
-length 0C = 12 bytes advertisement sent in step 3 has a single
-"Advertising Protocol Data Unit" (PDU):
+- For testing Bluetooth beacon Service Discovery on Linux, you will
+ need to suppress the avahi-daemon which provides DNS-SD Service
+ Discovery on UxPlay's Host system (replace `mask` and `stop` below
+ by `unmask` and `start` to restore DNS-SD service):
-- 0B FF 4C 00 09 06 03 30 C0 A8 01 FD: length 0B = 11 bytes,
- consisting of: FF ( type = manufacturer-specific) 4C 00
- (manufacturer code = 0x004c, Apple ) manufacturer data 09 06 03 30
- C0 A8 01 FD
+```{=html}
+
+```
+ $ sudo systemctl mask avahi-daemon.socket
+ $ sudo systemctl stop avahi-daemon
-The manufacturer data defined by Apple consists of a single Apple data
-unit: 09 (Apple type = AirPlay), 06 (Apple data length 6 bytes) Apple
-data 03 30 XX XX XX XX, broken down into 03 (flags: 0000 0011) 30 (a
-seed) XX XX XX XX (IPv4 network address, written as four hexadecimal
-octets in standard order). (Apple TV's use a random private "AdvAddr"
-address as described above, and periodically update it at about 20 min
-intervals, each time increasing the seed by 1.)
+An automated procedure for creating the beacon would presumably want to
+switch it on when uxplay starts, and off when it stops. The 20-byte file
+created when uxplay starts (and deleted when it stops) contains the PID
+as a uint32_t unsigned integer in the first 4 bytes, followed by up to
+the first 11 characters of the process name (usually "uxplay") as a
+null-terminated string, padded with zeroes to 16 bytes. This data can be
+used to test whether uxplay is actually running, including cases where
+it has segfaulted and not deleted the file.
-Apple TV's also insert a type-1 ("Flags") 2-byte PDU "`02 01 1A`" before
-the manufacturer-specific PDU, increasing the significant length of the
-message to 0xf = 15 bytes. It turns out that the "Flags" PDU is
-"optional" for advertisements like beacons that do not allow client
-connections: in our tests on v4.0 and later dongles, Service Discovery
-still worked fine after dropping the "Flags" PDU.
+This method above creates a beacon that identifies itself with a "public
+Advertising Address" (the MAC hardware address of the Bluetooth device).
+An Apple TV uses a private random address. If you wish to do that,
+change the sixth octet (the one following `0x03`) in Step 1 from "TxAdd"
+= `0x00` to TxAdd = `0x01`, and add an intermediate "step 1.5":
-Both Linux and Windows have high-level interfaces that support users
-sending Advertising PDU's, but restricted to type 0xff
-"manufacturer-specific-data" only, without any "Flags". These should be
-used for automating beacon setup, and are: (Linux) Bluez
-[LEAdvertisingManager1](https://github.com/bluez/bluez/blob/master/test/example-advertisement)
-and (Windows 10/11)
-[BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
-(with an
-[example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
+**Step 1.5** Choose 6 random bytes r1, r2, r3, r4, r5, r6, such as
+"`0x52 0xaa, 0xaa, 0x3a, 0xb4, 0x2f`", and use HCI LE command 0x0005 to
+set the random address:
-**We don't know if these instructions can be modified to advertise IPv6
-addresses: if you know of any verified support for Bluetooth LE IPv6
-Service Discovery in newer AppleTV models, please let us know. Simply
-replacing the 4-byte IPv4 address with a 16-byte IPv6 address (and
-adjusting the lengths at bytes 1, 5 and 10) does not seem to work,
-although perhaps we did not find the right value for byte 11 ("Apple
-Flags"). If Apple's Bluetooth LE Service Discovery has IPv6 support, we
-need to examine the beacon advertisement packet for IPv6 addresses with
-a Bluetooth sniffer.**
+ $sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
+ < HCI Command: ogf 0x08, ocf 0x0005, plen 6
+ 52 AA AA 3A B4 2F
+ > HCI Event: 0x0e plen 4
+ 02 05 20 00
+
+On a Bluetooth packet sniffer with wireshark, this address displays as:
+**Advertising Address: 2f:b4:3a:aa:aa:52**. In principle, random byte r6
+should be masked with 0x03 (r6 = r6 \| 0x03) to mark the address as a
+"static random private address", but Apple TV does not do this. In fact
+it updates to a new random Advertising Address every 20 mins or so,
+increasing the seed in the Apple Data by 1 each time. Apple TV's also
+add a length 2 type 0x01 ("Flags") Advertising PDU "`0x02 0x01 0x1a`" in
+front of the main type 0xff "Manufacturer-Specific Data" Advertising PDU
+in Step 2. This is "optional" for ADV_NONCONN_IND advertisement type,
+and testing shows that it can be dropped without affecting Service
+Discovery, which is fortunate because the high-level Linux and Windows
+interfaces mentioned earlier do not permit users to send a "Flags"-type
+PDU.
+
+- **Our current understanding is that Bluetooth LE AirPlay Service
+ Discovery only supports broadcast of IPv4 addresses. Please let us
+ know if this is incorrect, or if IPv6 support is introduced in the
+ future.**
# Troubleshooting
From 2c3fcdcf8ddf01608b196493d16f775678da24ab Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 26 Sep 2025 01:10:41 -0400
Subject: [PATCH 42/58] add -vrtp option (requested by @tovrstra )
---
README.html | 19 ++++++++++++--
README.md | 16 ++++++++++--
README.txt | 19 ++++++++++++--
renderers/video_renderer.c | 53 +++++++++++++++++++++-----------------
renderers/video_renderer.h | 2 +-
uxplay.1 | 7 +++++
uxplay.cpp | 17 ++++++++++--
7 files changed, 101 insertions(+), 32 deletions(-)
diff --git a/README.html b/README.html
index 2779dd3..cce5d90 100644
--- a/README.html
+++ b/README.html
@@ -21,6 +21,13 @@ href="#bluetooth-le-beacon-setup">given below. It is hoped
that users will submit Pull Requests contributing scripts for automating
beacon setup on all platforms. (Python may be an appropriate language
choice)
+NEW on github: option
+-vrtp <rest-of-pipeline> bypasses rendering by
+UxPlay, and instead transmits rtp packets of decrypted h264 or h265
+video to an external renderer (e.g. OBS Studio) at an address specified
+in rest-of-pipeline. (Note: this is video only, an option
+“-rtp” which muxes audio and video into a mpeg4 container still needs to
+be created: Pull Requests welcomed).
NEW on github: (for Linux/*BSD Desktop
Environments using D-Bus). New option -scrsv <n>
provides screensaver inhibition (e.g., to prevent screensaver function
@@ -1250,6 +1257,12 @@ display video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only
mode.
+-vrtp pipeline: forward rtp packets of
+decrypted video to somewhere else, without rendering. Uses rtph264pay or
+rtph265pay as appropriate: pipeline should start with any
+rtph26xpay options (such as config_interval= or aggregate-mode =),
+followed by a sending method: e.g.,
+"config-interval=1 ! udpsink host=127.0.0.1 port=5000“.
-v4l2 Video settings for hardware h264 video
decoding in the GPU by Video4Linux2. Equivalent to
-vd v4l2h264dec -vc v4l2convert.
@@ -1933,10 +1946,12 @@ an AppleTV6,2 with sourceVersion 380.20.1 (an AppleTV 4K 1st gen,
introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
Changelog
-xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option
+
xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option
(no file specified). (D-Bus based) option -scrsv to inhibit
screensaver while UxPlay is running (Linux/*BSD only). Add support for
-Service Discovery using a Bluetooth LE beacon.
+Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
+forwarding decrypted h264/5 video to an external renderer (e.g., OBS
+Studio)
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced
with -pw option. Update llhttp to v 9.3.0
1.72.1 2025-06-06 minor update: fix regression in -reg option; add
diff --git a/README.md b/README.md
index 9208d5b..ca43052 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,12 @@
__It is hoped that users will submit Pull Requests contributing scripts for automating beacon setup on all platforms.
(Python may be an appropriate language choice)__
+- **NEW on github**: option `-vrtp ` bypasses rendering by UxPlay, and instead
+ transmits rtp packets of decrypted h264 or h265 video to
+ an external renderer (e.g. OBS Studio) at an address specified in `rest-of-pipeline`.
+ (Note: this is video only, an option "-rtp" which muxes audio and video into a mpeg4 container still needs to be created:
+ Pull Requests welcomed).
+
- **NEW on github**: (for Linux/*BSD Desktop Environments using D-Bus). New option `-scrsv ` provides screensaver inhibition (e.g., to
prevent screensaver function while watching mirrored videos without keyboard or mouse
activity): n = 0 (off) n=1 (on during video activity) n=2 (always on while UxPlay is running).
@@ -1252,6 +1258,11 @@ video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only mode.
+**-vrtp *pipeline***: forward rtp packets of decrypted video to somewhere else, without rendering.
+Uses rtph264pay or rtph265pay as appropriate: *pipeline* should start with any
+rtph26xpay options (such as config_interval= or aggregate-mode =), followed by
+a sending method: *e.g.*, `"config-interval=1 ! udpsink host=127.0.0.1 port=5000`".
+
**-v4l2** Video settings for hardware h264 video decoding in the GPU by
Video4Linux2. Equivalent to `-vd v4l2h264dec -vc v4l2convert`.
@@ -1957,10 +1968,11 @@ introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
# Changelog
-xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option (no file
+xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option (no file
specified). (D-Bus based) option -scrsv to inhibit screensaver while UxPlay
is running (Linux/*BSD only). Add support for Service Discovery using a
-Bluetooth LE beacon.
+Bluetooth LE beacon. Add -vrtp option for forwarding decrypted h264/5 video
+to an external renderer (e.g., OBS Studio)
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with -pw
option. Update llhttp to v 9.3.0
diff --git a/README.txt b/README.txt
index 3e0017d..9091dcc 100644
--- a/README.txt
+++ b/README.txt
@@ -16,6 +16,13 @@
setup on all platforms. (Python may be an appropriate language
choice)**
+- **NEW on github**: option `-vrtp ` bypasses
+ rendering by UxPlay, and instead transmits rtp packets of decrypted
+ h264 or h265 video to an external renderer (e.g. OBS Studio) at an
+ address specified in `rest-of-pipeline`. (Note: this is video only,
+ an option "-rtp" which muxes audio and video into a mpeg4 container
+ still needs to be created: Pull Requests welcomed).
+
- **NEW on github**: (for Linux/\*BSD Desktop Environments using
D-Bus). New option `-scrsv ` provides screensaver inhibition
(e.g., to prevent screensaver function while watching mirrored
@@ -1276,6 +1283,12 @@ video), and only used to render audio, which will be AAC
lossily-compressed audio in mirror mode with unrendered video, and
superior-quality ALAC Apple Lossless audio in Airplay audio-only mode.
+**-vrtp *pipeline***: forward rtp packets of decrypted video to
+somewhere else, without rendering. Uses rtph264pay or rtph265pay as
+appropriate: *pipeline* should start with any rtph26xpay options (such
+as config_interval= or aggregate-mode =), followed by a sending method:
+*e.g.*, `"config-interval=1 ! udpsink host=127.0.0.1 port=5000`".
+
**-v4l2** Video settings for hardware h264 video decoding in the GPU by
Video4Linux2. Equivalent to `-vd v4l2h264dec -vc v4l2convert`.
@@ -2006,10 +2019,12 @@ what version UxPlay claims to be.
# Changelog
-xxxx 2025-08-11 Render Audio cover-art inside UxPlay with -ca option (no
+xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option (no
file specified). (D-Bus based) option -scrsv ``{=html} to inhibit
screensaver while UxPlay is running (Linux/\*BSD only). Add support for
-Service Discovery using a Bluetooth LE beacon.
+Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
+forwarding decrypted h264/5 video to an external renderer (e.g., OBS
+Studio)
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with
-pw option. Update llhttp to v 9.3.0
diff --git a/renderers/video_renderer.c b/renderers/video_renderer.c
index 9cea88e..96ce25e 100644
--- a/renderers/video_renderer.c
+++ b/renderers/video_renderer.c
@@ -222,11 +222,12 @@ GstElement *make_video_sink(const char *videosink, const char *videosink_options
return video_sink;
}
-void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
+void video_renderer_init(logger_t *render_logger, const char *server_name, videoflip_t videoflip[2], const char *parser, const char * rtp_pipeline,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support, guint playbin_version, const char *uri) {
GError *error = NULL;
GstCaps *caps = NULL;
+ bool rtp = (bool) strlen(rtp_pipeline);
hls_video = (uri != NULL);
/* videosink choices that are auto */
auto_videosink = (strstr(videosink, "autovideosink") || strstr(videosink, "fpsdisplaysink"));
@@ -332,30 +333,36 @@ void video_renderer_init(logger_t *render_logger, const char *server_name, video
g_string_append(launch, "queue ! ");
g_string_append(launch, parser);
g_string_append(launch, " ! ");
- g_string_append(launch, decoder);
+ if (!rtp) {
+ g_string_append(launch, decoder);
+ } else {
+ g_string_append(launch, "rtph264pay ");
+ g_string_append(launch, rtp_pipeline);
+ }
}
- g_string_append(launch, " ! ");
- append_videoflip(launch, &videoflip[0], &videoflip[1]);
- g_string_append(launch, converter);
- g_string_append(launch, " ! ");
- g_string_append(launch, "videoscale ! ");
- if (jpeg_pipeline) {
- g_string_append(launch, " imagefreeze allow-replace=TRUE ! ");
+ if (!rtp || jpeg_pipeline) {
+ g_string_append(launch, " ! ");
+ append_videoflip(launch, &videoflip[0], &videoflip[1]);
+ g_string_append(launch, converter);
+ g_string_append(launch, " ! ");
+ g_string_append(launch, "videoscale ! ");
+ if (jpeg_pipeline) {
+ g_string_append(launch, " imagefreeze allow-replace=TRUE ! ");
+ }
+ g_string_append(launch, videosink);
+ g_string_append(launch, " name=");
+ g_string_append(launch, videosink);
+ g_string_append(launch, "_");
+ g_string_append(launch, renderer_type[i]->codec);
+ g_string_append(launch, videosink_options);
+ if (video_sync && !jpeg_pipeline) {
+ g_string_append(launch, " sync=true");
+ sync = true;
+ } else {
+ g_string_append(launch, " sync=false");
+ sync = false;
+ }
}
- g_string_append(launch, videosink);
- g_string_append(launch, " name=");
- g_string_append(launch, videosink);
- g_string_append(launch, "_");
- g_string_append(launch, renderer_type[i]->codec);
- g_string_append(launch, videosink_options);
- if (video_sync && !jpeg_pipeline) {
- g_string_append(launch, " sync=true");
- sync = true;
- } else {
- g_string_append(launch, " sync=false");
- sync = false;
- }
-
if (!strcmp(renderer_type[i]->codec, h264)) {
char *pos = launch->str;
while ((pos = strstr(pos,h265))){
diff --git a/renderers/video_renderer.h b/renderers/video_renderer.h
index 8e96496..d280592 100644
--- a/renderers/video_renderer.h
+++ b/renderers/video_renderer.h
@@ -47,7 +47,7 @@ typedef enum videoflip_e {
typedef struct video_renderer_s video_renderer_t;
-void video_renderer_init (logger_t *logger, const char *server_name, videoflip_t videoflip[2], const char *parser,
+ void video_renderer_init (logger_t *logger, const char *server_name, videoflip_t videoflip[2], const char *parser, const char *rtp_pipeline,
const char *decoder, const char *converter, const char *videosink, const char *videosink_options,
bool initial_fullscreen, bool video_sync, bool h265_support, bool coverart_support,
guint playbin_version, const char *uri);
diff --git a/uxplay.1 b/uxplay.1
index 32bec7b..7a1fde2 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -98,6 +98,13 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.TP
\fB\-vs\fR 0 Streamed audio only, with no video display window.
.TP
+\fB\-vrtp\fI pl\fR Use rtph26[4,5]pay to send decoded video elsewhere: "pl"
+.IP
+ is the remaining pipeline, starting with rtph26*pay options:
+.IP
+ e.g. "config-interval=1 ! udpsink host=127.0.0.1 port=5000"
+.PP
+.TP
\fB\-v4l2\fR Use Video4Linux2 for GPU hardware h264 video decoding.
.TP
\fB\-bt709\fR Sometimes needed for Raspberry Pi models using Video4Linux2.
diff --git a/uxplay.cpp b/uxplay.cpp
index 218d629..1b6cd7b 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -190,6 +190,8 @@ static uint32_t rtptime_coverart_expired = 0;
static std::string artist;
static std::string coverart_artist;
static std::string ble_filename = "";
+static std::string rtp_pipeline = "";
+
//Support for D-Bus-based screensaver inhibition (org.freedesktop.ScreenSaver)
static unsigned int scrsv;
@@ -879,6 +881,9 @@ static void print_info (char *name) {
printf(" gtksink,waylandsink,kmssink,fbdevsink,osxvideosink,\n");
printf(" d3d11videosink,d3v12videosink, etc.\n");
printf("-vs 0 Streamed audio only, with no video display window\n");
+ printf("-vrtp pl Use rtph26[4,5]pay to send decoded video elsewhere: \"pl\"\n");
+ printf(" is the remaining pipeline, starting with rtph26*pay options:\n");
+ printf(" e.g. \"config-interval=1 ! udpsink host=127.0.0.1 port=5000\"\n");
printf("-v4l2 Use Video4Linux2 for GPU hardware h264 decoding\n");
printf("-bt709 Sometimes needed for Raspberry Pi models using Video4Linux2 \n");
printf("-srgb Display \"Full range\" [0-255] color, not \"Limited Range\"[16-235]\n");
@@ -1297,6 +1302,14 @@ static void parse_arguments (int argc, char *argv[]) {
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 == "-vrtp") {
+ if (!option_has_value(i, argc, arg, argv[i+1])) {
+ fprintf(stderr,"option \"-vrtp\" must be followed by a pipeline for sending the video stream:\n"
+ "e.g., \" ! udpsink host=127.0.0.1 port -= 5000\"\n");
+ exit(1);
+ }
+ rtp_pipeline.erase();
+ rtp_pipeline.append(argv[++i]);
} else if (arg == "-vdmp") {
dump_video = true;
if (i < argc - 1 && *argv[i+1] != '-') {
@@ -2817,7 +2830,7 @@ int main (int argc, char *argv[]) {
LOGI("audio_disabled");
}
if (use_video) {
- video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
+ video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(), rtp_pipeline.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
videosink_options.c_str(), fullscreen, video_sync, h265_support,
render_coverart, playbin_version, NULL);
@@ -2914,7 +2927,7 @@ int main (int argc, char *argv[]) {
raop_remove_known_connections(raop);
}
const char *uri = (url.empty() ? NULL : url.c_str());
- video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),
+ video_renderer_init(render_logger, server_name.c_str(), videoflip, video_parser.c_str(),rtp_pipeline.c_str(),
video_decoder.c_str(), video_converter.c_str(), videosink.c_str(),
videosink_options.c_str(), fullscreen, video_sync, h265_support,
render_coverart, playbin_version, uri);
From 37becd99062c4d6a59630865e973d79d6f2752c0 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 26 Sep 2025 21:14:17 -0400
Subject: [PATCH 43/58] update Bluetooth LE beacon support: set choice of TCP
port
---
README.html | 84 +++++++++++++++++++++++++++++++----------------------
README.md | 52 +++++++++++++++++++--------------
README.txt | 82 +++++++++++++++++++++++++++++----------------------
uxplay.cpp | 32 ++++++++++----------
4 files changed, 145 insertions(+), 105 deletions(-)
diff --git a/README.html b/README.html
index cce5d90..d474968 100644
--- a/README.html
+++ b/README.html
@@ -13,10 +13,10 @@ discovery using a Bluetooth LE “beacon” (as an alternative to
Bonjour/Rendezvous DNS-SD service discovery). The user must set up a
Bluetooth LE “beacon”, (a USB 4.0 or later “dongle” can be used). See
instructions below. The beacon runs independently of UxPlay and
-regularly broadcasts a Bluetooth LE (“Low Energy”) 44 byte packet
+regularly broadcasts a Bluetooth LE (“Low Energy”) 46 byte packet
informing nearby iOS/macOS devices of the local IPv4 network address of
-the UxPlay server, which they can use to contact it on TCP port 7000.
-Instructions for manually setting up such a beacon in Linux are given below. It is hoped
that users will submit Pull Requests contributing scripts for automating
beacon setup on all platforms. (Python may be an appropriate language
@@ -1455,31 +1455,42 @@ this to see even more of the GStreamer inner workings.
Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous) service
discovery, start it with the option
“-ble <path-to-writeable-file>”, which at startup
-writes a data file containing the uxplay process ID and process name,
-and is deleted when uxplay terminates normally. This file is not
+writes a data file containing the uxplay TCP port for receiving replies
+to the advertisement, plus the uxplay process ID and process name, and
+is deleted when uxplay terminates normally. This file is not
used in the simple manual method for creating a beacon described
below.
Bluetooth LE Service discovery uses a “beacon” broadcasting a simple
-12-byte advertisement “0B FF 4C 00 09 06 03 30 XX XX XX XX”
-where XX XX XX XX is an IPv4 internet address of the UxPlay host
-translated into hexadecimal octets: For example,
-“XX XX XX XX” = “C0 A8 01 FD” means
-192.168.2.253. UxPlay must be able to receive messages on TCP port 7000
-at this address. The uxplay option “-p” sets up uxplay to
-listen on port 7000 for these messages.
-The full translation of this message is that it has length 0B = 0x0b
-= 11 octets, and is a single “Advertising Protocol Data Unit” (PDU) of
+14-byte advertisement
+“0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY” where XX XX XX
+XX is an IPv4 internet address (and port YY YY) of the UxPlay host
+translated into hexadecimal octets. For example,
+“XX XX XX XX YY YY” = “C0 A8 01 FD 1B 58”
+means 192.168.2.253 port 0x1b58 (decimal value 7000). UxPlay must be
+able to receive messages on this TCP port at this address. The uxplay
+option “-p” sets up uxplay to listen on the default port
+7000 for these messages, as used in the example above. Otherwise the
+port in the beacon message should be the first (<n>)
+of the 3 open TCP ports specified with uxplay option
+-p <n>. If the -p option is not used
+(which is only possible if there is no active firewall) the TCP port is
+selected at random, and its value must be taken from the beginning of
+the file written with the -ble option.
+The full translation of this message is that it has length 0D = 0x0d
+= 13 octets, and is a single “Advertising Protocol Data Unit” (PDU) of
type “FF”, called “Manufacturer-Specific Data”, with
“manufacturer code” “4C 00” = 0x004c = Apple (note the
reversal of octet order when two octets are combined to make a two-byte
-unsigned short integer), and “09 06 03 30 XX XX XX XX” is
-the Apple-specific data.
+unsigned short integer), and
+“09 08 13 30 XX XX XX XX YY YY” is the Apple-specific
+data.
The Apple-specific data contains a single Apple Data Unit with Apple
-type = 09 (Airplay), Apple Data length 06 (0x06 = 6 octets) and Apple
-Data “03 30 XX XX XX XX” where 03 = 0000 0011 is Apple
-Flags, 30 is a seed (which will be ignored), and XX XX XX XX is the IPv4
-internet address. This is smaller than the “iBeacon” Apple Data Unit,
-which has Apple type 02 and Apple length 15 (0x15 = 21 octets).
+type = 09 (Airplay), Apple Data length 08 (0x08 = 8 octets) and Apple
+Data “13 30 XX XX XX XX YY YY” where 13 = 0001 0011 is
+Apple Flags, 30 is a seed (which will be ignored), XX XX XX XX is the
+IPv4 internet address and YY YY is the port. This is smaller than the
+“iBeacon” Apple Data Unit, which has Apple type 02 and Apple length 15
+(0x15 = 21 octets).
In addition to creating the message, we need to set the “Advertising
type” (ADV_NONCONN_IND) and “Advertising interval” range [AdvMin,
AdvMax], where 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000
@@ -1547,13 +1558,14 @@ three advertising channels.
Step 2. Set the advertising message with HCI LE
command 0x0008. For this command, hcitool requires a 32 octet message
after sudo hcitool -i hci0 cmd 0x08 0x0008: The first octet
-is the length 0C = 0x0c = 12 of the “significant part” of the following
-31 octets, followed by the 12 octets of the advertisement, then padded
-with 19 zeroes to a total length of 32 octets. The example below sends
-an IPv4 address 192.168.1.253 as “0xc0 0xa8 0x01 0xfd”:
-$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+is the length 0E = 0x0e = 14 of the “significant part” of the following
+31 octets, followed by the 14 octets of the advertisement, then padded
+with 17 zeroes to a total length of 32 octets. The example below sends
+an IPv4 address 192.168.1.253 as “0xc0 0xa8 0x01 0xfd” and
+the TCP port as 0x1b 0x58 (port 7000 = 0x1b58):
+$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0e 0x0d 0xff 0x4c 0x00 0x09 0x08 0x13 0x30 0xc0 0xa8 0x01 0xfd 0x1b 0x58 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
< HCI Command: ogf 0x08, ocf 0x0008, plen 32
- 0C 0B FF 4C 00 09 06 03 30 C0 A8 01 FD 00 00 00 00 00 00 00
+ 0E 0D FF 4C 00 09 08 13 30 C0 A8 01 FD 1B 58 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
> HCI Event: 0x0e plen 4
01 08 20 00
@@ -1564,7 +1576,7 @@ an IPv4 address 192.168.1.253 as “0xc0 0xa8 0x01 0xfd”:
01
> HCI Event: 0x0e plen 4
02 0A 20 00
-The full length of the broadcasted beacon message is 43 bytes. To
+
The full length of the broadcasted beacon message is 46 bytes. To
stop the beacon, use this command to send the 1-byte message
“0x00” = “off”.
@@ -1577,13 +1589,17 @@ restore DNS-SD service):
$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
An automated procedure for creating the beacon would presumably want
-to switch it on when uxplay starts, and off when it stops. The 20-byte
+to switch it on when uxplay starts, and off when it stops. The 22-byte
file created when uxplay starts (and deleted when it stops) contains the
-PID as a uint32_t unsigned integer in the first 4 bytes, followed by up
-to the first 11 characters of the process name (usually “uxplay”) as a
-null-terminated string, padded with zeroes to 16 bytes. This data can be
-used to test whether uxplay is actually running, including cases where
-it has segfaulted and not deleted the file.
+RAOP port as a uint16_t unsigned short, in the first 2 bytes, followed
+by the uxplay PID as a uint32_t unsigned integer in the next 4 bytes,
+then followed by up to the first 11 characters of the process name
+(usually “uxplay”) as a null-terminated string, padded with zeroes to 16
+bytes. The port data identifies the port on the Host that uxplay listens
+on, which should be included along with the Host IPv4 address in the
+advertisement broadcast by the beacon. The remaining data can be used to
+check whether uxplay is actually running, including cases where it has
+segfaulted and not deleted the file.
This method above creates a beacon that identifies itself with a
“public Advertising Address” (the MAC hardware address of the Bluetooth
device). An Apple TV uses a private random address. If you wish to do
diff --git a/README.md b/README.md
index ca43052..4f916f4 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,8 @@
- **NEW on github**: Support for **service discovery using a Bluetooth LE "beacon"** (as an alternative to Bonjour/Rendezvous DNS-SD
service discovery). The user must set up a Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used). See instructions
- below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 44 byte packet informing nearby iOS/macOS devices of
- the local IPv4 network address of the UxPlay server, which they can use to contact it on TCP port 7000.
+ below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet informing nearby iOS/macOS devices of
+ the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on.
Instructions for manually setting up such a beacon in Linux are [given below](#bluetooth-le-beacon-setup).
__It is hoped that users will submit Pull Requests contributing scripts for automating beacon setup on all platforms.
(Python may be an appropriate language choice)__
@@ -1471,24 +1471,28 @@ GStreamer inner workings.
To allow UxPlay to work with Bluetooth Low Energy (LE) Service Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous)
service discovery, start it with the option "`-ble `", which at startup writes a data file containing
-the uxplay process ID and process name, and is deleted when uxplay terminates normally. **This file
+the uxplay TCP port for receiving replies to the advertisement, plus the uxplay process ID and process name, and is deleted when uxplay terminates normally. **This file
is not used in the simple manual method for creating a beacon described below**.
-Bluetooth LE Service discovery uses a "beacon" broadcasting a simple 12-byte
-advertisement "`0B FF 4C 00 09 06 03 30 XX XX XX XX`" where XX XX XX XX is an IPv4 internet
-address of the UxPlay host translated into hexadecimal octets: For
-example, "`XX XX XX XX`" = "``C0 A8 01 FD``" means 192.168.2.253. UxPlay
-must be able to receive messages on TCP port 7000 at this
-address. The uxplay option "`-p`" sets up uxplay to listen on port 7000 for these messages.
+Bluetooth LE Service discovery uses a "beacon" broadcasting a simple 14-byte
+advertisement "`0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY`" where XX XX XX XX is an IPv4 internet
+address (and port YY YY) of the UxPlay host translated into hexadecimal octets. For
+example, "`XX XX XX XX YY YY`" = "``C0 A8 01 FD 1B 58``" means 192.168.2.253 port 0x1b58 (decimal value 7000). UxPlay
+must be able to receive messages on this TCP port at this
+address. The uxplay option "`-p`" sets up uxplay to listen on the default port 7000 for these messages, as used in the
+example above. Otherwise the port in the beacon message should
+be the first (``) of the 3 open TCP ports specified with uxplay option ``-p ``. If
+the `-p` option is not used (which is only possible if there is no active firewall) the TCP port is selected at random, and its value
+must be taken from the beginning of the file written with the `-ble` option.
-The full translation of this message is that it has length 0B = 0x0b = 11 octets, and is a single "Advertising Protocol Data Unit" (PDU) of type "`FF`",
+The full translation of this message is that it has length 0D = 0x0d = 13 octets, and is a single "Advertising Protocol Data Unit" (PDU) of type "`FF`",
called "Manufacturer-Specific Data", with "manufacturer code" "`4C 00`" = 0x004c = Apple (note the reversal of octet order when
-two octets are combined to make a two-byte unsigned short integer), and "`09 06 03 30 XX XX XX XX`" is the Apple-specific data.
+two octets are combined to make a two-byte unsigned short integer), and "`09 08 13 30 XX XX XX XX YY YY`" is the Apple-specific data.
-The Apple-specific data contains a single Apple Data Unit with Apple type = 09 (Airplay), Apple Data length 06 (0x06 = 6 octets) and
-Apple Data "`03 30 XX XX XX XX`" where 03 = 0000 0011 is Apple Flags, 30 is a seed (which will be ignored), and XX XX XX XX
-is the IPv4 internet address. This is smaller than the "iBeacon" Apple Data Unit, which has Apple type 02 and Apple length 15 (0x15 = 21 octets).
+The Apple-specific data contains a single Apple Data Unit with Apple type = 09 (Airplay), Apple Data length 08 (0x08 = 8 octets) and
+Apple Data "`13 30 XX XX XX XX YY YY`" where 13 = 0001 0011 is Apple Flags, 30 is a seed (which will be ignored), XX XX XX XX
+is the IPv4 internet address and YY YY is the port. This is smaller than the "iBeacon" Apple Data Unit, which has Apple type 02 and Apple length 15 (0x15 = 21 octets).
In addition to creating the message, we need to set the "Advertising type" (ADV_NONCONN_IND) and "Advertising interval" range [AdvMin, AdvMax],
where 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000 = 10.24 sec
@@ -1555,14 +1559,14 @@ advertising channels.
An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120 ("`0x20 0x01`").
**Step 2.** Set the advertising message with HCI LE command 0x0008. For this command, hcitool requires a 32 octet message after
-`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0C = 0x0c = 12 of the "significant part" of the following 31 octets,
-followed by the 12 octets of the advertisement, then padded with 19 zeroes to a total length of 32 octets. The example below sends an
-IPv4 address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`":
+`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0E = 0x0e = 14 of the "significant part" of the following 31 octets,
+followed by the 14 octets of the advertisement, then padded with 17 zeroes to a total length of 32 octets. The example below sends an
+IPv4 address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`" and the TCP port as 0x1b 0x58 (port 7000 = 0x1b58):
```
-$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0e 0x0d 0xff 0x4c 0x00 0x09 0x08 0x13 0x30 0xc0 0xa8 0x01 0xfd 0x1b 0x58 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
< HCI Command: ogf 0x08, ocf 0x0008, plen 32
- 0C 0B FF 4C 00 09 06 03 30 C0 A8 01 FD 00 00 00 00 00 00 00
+ 0E 0D FF 4C 00 09 08 13 30 C0 A8 01 FD 1B 58 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
> HCI Event: 0x0e plen 4
01 08 20 00
@@ -1577,7 +1581,7 @@ $ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
> HCI Event: 0x0e plen 4
02 0A 20 00
```
-The full length of the broadcasted beacon message is 43 bytes.
+The full length of the broadcasted beacon message is 46 bytes.
To stop the beacon, use this command to send the 1-byte message "`0x00`" = "off".
@@ -1594,9 +1598,13 @@ $ sudo systemctl stop avahi-daemon
An automated procedure for creating the beacon would presumably want to switch it on when uxplay starts, and off when it
-stops. The 20-byte file created when uxplay starts (and deleted when it stops) contains the PID as a uint32_t unsigned integer in the first 4 bytes,
+stops. The 22-byte file created when uxplay starts (and deleted when it stops) contains the RAOP port as a uint16_t unsigned short,
+in the first 2 bytes, followed by
+the uxplay PID as a uint32_t unsigned integer in the next 4 bytes, then
followed by up to the first
-11 characters of the process name (usually "uxplay") as a null-terminated string, padded with zeroes to 16 bytes. This data can be used to test
+11 characters of the process name (usually "uxplay") as a null-terminated string, padded with zeroes to 16 bytes. The port data
+identifies the port on the Host that uxplay listens on, which should be included along with the Host IPv4 address
+in the advertisement broadcast by the beacon. The remaining data can be used to check
whether uxplay is actually running, including cases where it has segfaulted and not deleted the file.
diff --git a/README.txt b/README.txt
index 9091dcc..0fe177f 100644
--- a/README.txt
+++ b/README.txt
@@ -7,12 +7,12 @@
service discovery). The user must set up a Bluetooth LE "beacon", (a
USB 4.0 or later "dongle" can be used). See instructions below. The
beacon runs independently of UxPlay and regularly broadcasts a
- Bluetooth LE ("Low Energy") 44 byte packet informing nearby
+ Bluetooth LE ("Low Energy") 46 byte packet informing nearby
iOS/macOS devices of the local IPv4 network address of the UxPlay
- server, which they can use to contact it on TCP port 7000.
- Instructions for manually setting up such a beacon in Linux are
- [given below](#bluetooth-le-beacon-setup). **It is hoped that users
- will submit Pull Requests contributing scripts for automating beacon
+ server, and which TCP port to contact UxPlay on. Instructions for
+ manually setting up such a beacon in Linux are [given
+ below](#bluetooth-le-beacon-setup). **It is hoped that users will
+ submit Pull Requests contributing scripts for automating beacon
setup on all platforms. (Python may be an appropriate language
choice)**
@@ -1497,32 +1497,41 @@ this to see even more of the GStreamer inner workings.
To allow UxPlay to work with Bluetooth Low Energy (LE) Service
Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous) service
discovery, start it with the option "`-ble `",
-which at startup writes a data file containing the uxplay process ID and
+which at startup writes a data file containing the uxplay TCP port for
+receiving replies to the advertisement, plus the uxplay process ID and
process name, and is deleted when uxplay terminates normally. **This
file is not used in the simple manual method for creating a beacon
described below**.
Bluetooth LE Service discovery uses a "beacon" broadcasting a simple
-12-byte advertisement "`0B FF 4C 00 09 06 03 30 XX XX XX XX`" where XX
-XX XX XX is an IPv4 internet address of the UxPlay host translated into
-hexadecimal octets: For example, "`XX XX XX XX`" = "`C0 A8 01 FD`" means
-192.168.2.253. UxPlay must be able to receive messages on TCP port 7000
-at this address. The uxplay option "`-p`" sets up uxplay to listen on
-port 7000 for these messages.
+14-byte advertisement "`0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY`"
+where XX XX XX XX is an IPv4 internet address (and port YY YY) of the
+UxPlay host translated into hexadecimal octets. For example,
+"`XX XX XX XX YY YY`" = "`C0 A8 01 FD 1B 58`" means 192.168.2.253 port
+0x1b58 (decimal value 7000). UxPlay must be able to receive messages on
+this TCP port at this address. The uxplay option "`-p`" sets up uxplay
+to listen on the default port 7000 for these messages, as used in the
+example above. Otherwise the port in the beacon message should be the
+first (``) of the 3 open TCP ports specified with uxplay option
+`-p `. If the `-p` option is not used (which is only possible if
+there is no active firewall) the TCP port is selected at random, and its
+value must be taken from the beginning of the file written with the
+`-ble` option.
-The full translation of this message is that it has length 0B = 0x0b =
-11 octets, and is a single "Advertising Protocol Data Unit" (PDU) of
+The full translation of this message is that it has length 0D = 0x0d =
+13 octets, and is a single "Advertising Protocol Data Unit" (PDU) of
type "`FF`", called "Manufacturer-Specific Data", with "manufacturer
code" "`4C 00`" = 0x004c = Apple (note the reversal of octet order when
two octets are combined to make a two-byte unsigned short integer), and
-"`09 06 03 30 XX XX XX XX`" is the Apple-specific data.
+"`09 08 13 30 XX XX XX XX YY YY`" is the Apple-specific data.
The Apple-specific data contains a single Apple Data Unit with Apple
-type = 09 (Airplay), Apple Data length 06 (0x06 = 6 octets) and Apple
-Data "`03 30 XX XX XX XX`" where 03 = 0000 0011 is Apple Flags, 30 is a
-seed (which will be ignored), and XX XX XX XX is the IPv4 internet
-address. This is smaller than the "iBeacon" Apple Data Unit, which has
-Apple type 02 and Apple length 15 (0x15 = 21 octets).
+type = 09 (Airplay), Apple Data length 08 (0x08 = 8 octets) and Apple
+Data "`13 30 XX XX XX XX YY YY`" where 13 = 0001 0011 is Apple Flags, 30
+is a seed (which will be ignored), XX XX XX XX is the IPv4 internet
+address and YY YY is the port. This is smaller than the "iBeacon" Apple
+Data Unit, which has Apple type 02 and Apple length 15 (0x15 = 21
+octets).
In addition to creating the message, we need to set the "Advertising
type" (ADV_NONCONN_IND) and "Advertising interval" range \[AdvMin,
@@ -1596,15 +1605,16 @@ An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120
**Step 2.** Set the advertising message with HCI LE command 0x0008. For
this command, hcitool requires a 32 octet message after
-`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0C
-= 0x0c = 12 of the "significant part" of the following 31 octets,
-followed by the 12 octets of the advertisement, then padded with 19
+`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0E
+= 0x0e = 14 of the "significant part" of the following 31 octets,
+followed by the 14 octets of the advertisement, then padded with 17
zeroes to a total length of 32 octets. The example below sends an IPv4
-address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`":
+address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`" and the TCP port as
+0x1b 0x58 (port 7000 = 0x1b58):
- $ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0c 0x0b 0xff 0x4c 0x00 0x09 0x06 0x03 0x30 0xc0 0xa8 0x01 0xfd 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+ $ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0e 0x0d 0xff 0x4c 0x00 0x09 0x08 0x13 0x30 0xc0 0xa8 0x01 0xfd 0x1b 0x58 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
< HCI Command: ogf 0x08, ocf 0x0008, plen 32
- 0C 0B FF 4C 00 09 06 03 30 C0 A8 01 FD 00 00 00 00 00 00 00
+ 0E 0D FF 4C 00 09 08 13 30 C0 A8 01 FD 1B 58 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
> HCI Event: 0x0e plen 4
01 08 20 00
@@ -1618,7 +1628,7 @@ with HCI LE command 0x000a = 10:
> HCI Event: 0x0e plen 4
02 0A 20 00
-The full length of the broadcasted beacon message is 43 bytes. To stop
+The full length of the broadcasted beacon message is 46 bytes. To stop
the beacon, use this command to send the 1-byte message "`0x00`" =
"off".
@@ -1634,13 +1644,17 @@ the beacon, use this command to send the 1-byte message "`0x00`" =
$ sudo systemctl stop avahi-daemon
An automated procedure for creating the beacon would presumably want to
-switch it on when uxplay starts, and off when it stops. The 20-byte file
-created when uxplay starts (and deleted when it stops) contains the PID
-as a uint32_t unsigned integer in the first 4 bytes, followed by up to
-the first 11 characters of the process name (usually "uxplay") as a
-null-terminated string, padded with zeroes to 16 bytes. This data can be
-used to test whether uxplay is actually running, including cases where
-it has segfaulted and not deleted the file.
+switch it on when uxplay starts, and off when it stops. The 22-byte file
+created when uxplay starts (and deleted when it stops) contains the RAOP
+port as a uint16_t unsigned short, in the first 2 bytes, followed by the
+uxplay PID as a uint32_t unsigned integer in the next 4 bytes, then
+followed by up to the first 11 characters of the process name (usually
+"uxplay") as a null-terminated string, padded with zeroes to 16 bytes.
+The port data identifies the port on the Host that uxplay listens on,
+which should be included along with the Host IPv4 address in the
+advertisement broadcast by the beacon. The remaining data can be used to
+check whether uxplay is actually running, including cases where it has
+segfaulted and not deleted the file.
This method above creates a beacon that identifies itself with a "public
Advertising Address" (the MAC hardware address of the Bluetooth device).
diff --git a/uxplay.cpp b/uxplay.cpp
index 1b6cd7b..2f06242 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -361,8 +361,9 @@ static int write_bledata( const uint32_t *pid, const char *process_name, const c
size_t len = strlen(process_name);
memcpy (name, process_name, (len > 15 ? 15 :len));
FILE *fp = fopen(filename, "wb");
- size_t count = fwrite(pid, sizeof (uint32_t), 1, fp);
- count *= sizeof(uint32_t);
+ printf("port %u\n", raop_port);
+ size_t count = sizeof(uint16_t) * fwrite(&raop_port, sizeof(uint16_t), 1, fp);
+ count += sizeof(uint32_t) * fwrite(pid, sizeof(uint32_t), 1, fp);
count += fwrite(name, 1, sizeof(name), fp);
fclose(fp);
return (int) count;
@@ -2871,19 +2872,6 @@ int main (int argc, char *argv[]) {
write_metadata(metadata_filename.c_str(), "no data\n");
}
-#define PID_MAX 4194304 // 2^22
- if (ble_filename.length()) {
-#ifdef _WIN_32
- DWORD pid = GetCurrentProcessId();
- g_assert(pid <= PID_MAX);
-#else
- pid_t pid = getpid();
- g_assert (pid <= PID_MAX && pid >= 0);
-#endif
- write_bledata((uint32_t *) &pid, argv[0], ble_filename.c_str());
- LOGI("Bluetooth LE beacon-based service discovery is possible: PID data written to %s", ble_filename.c_str());
- }
-
/* set default resolutions for h264 or h265*/
if (!display[0] && !display[1]) {
if (h265_support) {
@@ -2902,6 +2890,20 @@ int main (int argc, char *argv[]) {
stop_dnssd();
goto cleanup;
}
+
+#define PID_MAX 4194304 // 2^22
+ if (ble_filename.length()) {
+#ifdef _WIN_32
+ DWORD pid = GetCurrentProcessId();
+ g_assert(pid <= PID_MAX);
+#else
+ pid_t pid = getpid();
+ g_assert (pid <= PID_MAX && pid >= 0);
+#endif
+ write_bledata((uint32_t *) &pid, argv[0], ble_filename.c_str());
+ LOGI("Bluetooth LE beacon-based service discovery is possible: PID data written to %s", ble_filename.c_str());
+ }
+
if (register_dnssd()) {
stop_raop_server();
stop_dnssd();
From 243efdeca40797ff1a1fee0638d4c98272639562 Mon Sep 17 00:00:00 2001
From: fduncanh <72711181+fduncanh@users.noreply.github.com>
Date: Tue, 7 Oct 2025 18:38:32 +0900
Subject: [PATCH 44/58] Update README (beacon details)
---
README.html | 33 +++++++++++++++++++++------------
README.md | 19 ++++++++++---------
README.txt | 33 +++++++++++++++++++++------------
3 files changed, 52 insertions(+), 33 deletions(-)
diff --git a/README.html b/README.html
index d474968..610a2de 100644
--- a/README.html
+++ b/README.html
@@ -1466,7 +1466,7 @@ below.
XX is an IPv4 internet address (and port YY YY) of the UxPlay host
translated into hexadecimal octets. For example,
“XX XX XX XX YY YY” = “C0 A8 01 FD 1B 58”
-means 192.168.2.253 port 0x1b58 (decimal value 7000). UxPlay must be
+means 192.168.1.253 port 0x1b58 (decimal value 7000). UxPlay must be
able to receive messages on this TCP port at this address. The uxplay
option “-p” sets up uxplay to listen on the default port
7000 for these messages, as used in the example above. Otherwise the
@@ -1589,17 +1589,26 @@ restore DNS-SD service):
$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
An automated procedure for creating the beacon would presumably want
-to switch it on when uxplay starts, and off when it stops. The 22-byte
-file created when uxplay starts (and deleted when it stops) contains the
-RAOP port as a uint16_t unsigned short, in the first 2 bytes, followed
-by the uxplay PID as a uint32_t unsigned integer in the next 4 bytes,
-then followed by up to the first 11 characters of the process name
-(usually “uxplay”) as a null-terminated string, padded with zeroes to 16
-bytes. The port data identifies the port on the Host that uxplay listens
-on, which should be included along with the Host IPv4 address in the
-advertisement broadcast by the beacon. The remaining data can be used to
-check whether uxplay is actually running, including cases where it has
-segfaulted and not deleted the file.
+to switch it on when uxplay starts, and off when it stops. It has the
+task of determing a host IPv4 address that the client can use to reach
+uxplay. The 22-byte file created when uxplay starts (and deleted when it
+stops) contains the RAOP port as a uint16_t unsigned short, in the first
+2 bytes, followed by the uxplay PID as a uint32_t unsigned integer in
+the next 4 bytes, then followed by up to the first 15 characters of the
+process name (usually “uxplay”) as a null-terminated string, padded with
+zeroes to 16 bytes. The port data identifies the port on the Host that
+uxplay listens on, which should be included along with the Host IPv4
+address in the advertisement broadcast by the beacon. The path to this
+file is needed as the only input by the procedure when it is started.
+The presence of the file should be checked at regular intervals (once
+per second?). If it is absent, uxplay has stopped running, but if it
+exists the process ID and process name of that PID should be checked to
+handle cases where a new uxplay process has started, or if uxplay has
+exited abnormally and failed to delete the file. (While it is probably
+not an important use case, the possibility of concurrent uxplay
+processes listening on different ports and writing different files could
+be handled: the advertising protocol allows cycling between different
+messages.)
This method above creates a beacon that identifies itself with a
“public Advertising Address” (the MAC hardware address of the Bluetooth
device). An Apple TV uses a private random address. If you wish to do
diff --git a/README.md b/README.md
index 4f916f4..b478591 100644
--- a/README.md
+++ b/README.md
@@ -1478,7 +1478,7 @@ is not used in the simple manual method for creating a beacon described below**
Bluetooth LE Service discovery uses a "beacon" broadcasting a simple 14-byte
advertisement "`0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY`" where XX XX XX XX is an IPv4 internet
address (and port YY YY) of the UxPlay host translated into hexadecimal octets. For
-example, "`XX XX XX XX YY YY`" = "``C0 A8 01 FD 1B 58``" means 192.168.2.253 port 0x1b58 (decimal value 7000). UxPlay
+example, "`XX XX XX XX YY YY`" = "``C0 A8 01 FD 1B 58``" means 192.168.1.253 port 0x1b58 (decimal value 7000). UxPlay
must be able to receive messages on this TCP port at this
address. The uxplay option "`-p`" sets up uxplay to listen on the default port 7000 for these messages, as used in the
example above. Otherwise the port in the beacon message should
@@ -1595,19 +1595,20 @@ $ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
```
-
-
An automated procedure for creating the beacon would presumably want to switch it on when uxplay starts, and off when it
-stops. The 22-byte file created when uxplay starts (and deleted when it stops) contains the RAOP port as a uint16_t unsigned short,
+stops. It has the task of determing a host IPv4 address that the client can use to reach uxplay.
+The 22-byte file created when uxplay starts (and deleted when it stops) contains the RAOP port as a uint16_t unsigned short,
in the first 2 bytes, followed by
the uxplay PID as a uint32_t unsigned integer in the next 4 bytes, then
followed by up to the first
-11 characters of the process name (usually "uxplay") as a null-terminated string, padded with zeroes to 16 bytes. The port data
+15 characters of the process name (usually "uxplay") as a null-terminated string, padded with zeroes to 16 bytes. The port data
identifies the port on the Host that uxplay listens on, which should be included along with the Host IPv4 address
-in the advertisement broadcast by the beacon. The remaining data can be used to check
-whether uxplay is actually running, including cases where it has segfaulted and not deleted the file.
-
-
+in the advertisement broadcast by the beacon. The path to this file is needed as the only input by the procedure when it is started.
+The presence of the file should be checked at regular intervals (once per second?). If it is absent, uxplay has stopped running,
+but if it exists the process ID and process name of that PID should be checked to handle cases where a new uxplay process has
+started, or if uxplay has exited abnormally and failed to delete the file. (While it is probably not an important use case, the possibility of
+concurrent uxplay processes listening on different ports and writing different files could be handled: the advertising protocol allows
+cycling between different messages.)
This method above creates a beacon that identifies itself with a "public Advertising Address" (the MAC hardware address of
the Bluetooth device). An Apple TV uses a private random address. If you wish to do that, change the sixth octet (the one following `0x03`)
diff --git a/README.txt b/README.txt
index 0fe177f..4667087 100644
--- a/README.txt
+++ b/README.txt
@@ -1507,7 +1507,7 @@ Bluetooth LE Service discovery uses a "beacon" broadcasting a simple
14-byte advertisement "`0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY`"
where XX XX XX XX is an IPv4 internet address (and port YY YY) of the
UxPlay host translated into hexadecimal octets. For example,
-"`XX XX XX XX YY YY`" = "`C0 A8 01 FD 1B 58`" means 192.168.2.253 port
+"`XX XX XX XX YY YY`" = "`C0 A8 01 FD 1B 58`" means 192.168.1.253 port
0x1b58 (decimal value 7000). UxPlay must be able to receive messages on
this TCP port at this address. The uxplay option "`-p`" sets up uxplay
to listen on the default port 7000 for these messages, as used in the
@@ -1644,17 +1644,26 @@ the beacon, use this command to send the 1-byte message "`0x00`" =
$ sudo systemctl stop avahi-daemon
An automated procedure for creating the beacon would presumably want to
-switch it on when uxplay starts, and off when it stops. The 22-byte file
-created when uxplay starts (and deleted when it stops) contains the RAOP
-port as a uint16_t unsigned short, in the first 2 bytes, followed by the
-uxplay PID as a uint32_t unsigned integer in the next 4 bytes, then
-followed by up to the first 11 characters of the process name (usually
-"uxplay") as a null-terminated string, padded with zeroes to 16 bytes.
-The port data identifies the port on the Host that uxplay listens on,
-which should be included along with the Host IPv4 address in the
-advertisement broadcast by the beacon. The remaining data can be used to
-check whether uxplay is actually running, including cases where it has
-segfaulted and not deleted the file.
+switch it on when uxplay starts, and off when it stops. It has the task
+of determing a host IPv4 address that the client can use to reach
+uxplay. The 22-byte file created when uxplay starts (and deleted when it
+stops) contains the RAOP port as a uint16_t unsigned short, in the first
+2 bytes, followed by the uxplay PID as a uint32_t unsigned integer in
+the next 4 bytes, then followed by up to the first 15 characters of the
+process name (usually "uxplay") as a null-terminated string, padded with
+zeroes to 16 bytes. The port data identifies the port on the Host that
+uxplay listens on, which should be included along with the Host IPv4
+address in the advertisement broadcast by the beacon. The path to this
+file is needed as the only input by the procedure when it is started.
+The presence of the file should be checked at regular intervals (once
+per second?). If it is absent, uxplay has stopped running, but if it
+exists the process ID and process name of that PID should be checked to
+handle cases where a new uxplay process has started, or if uxplay has
+exited abnormally and failed to delete the file. (While it is probably
+not an important use case, the possibility of concurrent uxplay
+processes listening on different ports and writing different files could
+be handled: the advertising protocol allows cycling between different
+messages.)
This method above creates a beacon that identifies itself with a "public
Advertising Address" (the MAC hardware address of the Bluetooth device).
From 6d899820fb3348e4514e69bf1315eecc9d1313df Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 24 Oct 2025 18:57:44 -0400
Subject: [PATCH 45/58] show zone id before assertion (local_zone_id =
remote_zone_id) coredump
---
lib/httpd.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/lib/httpd.c b/lib/httpd.c
index db141ce..df939d9 100644
--- a/lib/httpd.c
+++ b/lib/httpd.c
@@ -292,6 +292,12 @@ httpd_accept_connection(httpd_t *httpd, int server_fd, int is_ipv6)
logger_log(httpd->logger, LOGGER_INFO, "Accepted %s client on socket %d, port %u",
(is_ipv6 ? "IPv6" : "IPv4"), fd, port);
remote = netutils_get_address(&remote_saddr, &remote_len, &remote_zone_id, NULL);
+
+ // is it correct that ipv6 link-local local and remote zone id should be the same, as asserted below?
+ if (local_zone_id != remote_zone_id) {
+ logger_log(httpd->logger, LOGGER_INFO, "ipv6 zone_id mismatch: local_zone_id = %u, remote_zone_id = %u",
+ local_zone_id, remote_zone_id);
+ }
assert (local_zone_id == remote_zone_id);
ret = httpd_add_connection(httpd, fd, local, local_len, remote, remote_len, local_zone_id);
From 63f62e9f748bd430726c8ebaf4f55f4020cdc61c Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 26 Oct 2025 01:48:28 -0400
Subject: [PATCH 46/58] add a python3 DBUs Bluetooth LE Service Discovery
beacon controller
---
Bluetooth_LE_beacon/dbus/uxplay-beacon.py | 456 ++++++++++++++++++++++
CMakeLists.txt | 8 +
README.html | 261 ++++---------
README.md | 197 ++--------
README.txt | 264 ++++---------
uxplay-beacon.1 | 45 +++
uxplay.1 | 6 +-
uxplay.cpp | 21 +-
8 files changed, 705 insertions(+), 553 deletions(-)
create mode 100644 Bluetooth_LE_beacon/dbus/uxplay-beacon.py
create mode 100644 uxplay-beacon.1
diff --git a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
new file mode 100644
index 0000000..8c4e863
--- /dev/null
+++ b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
@@ -0,0 +1,456 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# adapted from https://github.com/bluez/bluez/blob/master/test/example-advertisement
+#----------------------------------------------------------------
+# a standalone python-3.6 or later DBus-based AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
+# (c) F. Duncanh, October 2025
+
+import argparse
+import gi
+import os
+import sys
+import psutil
+import struct
+import socket
+
+from gi.repository import GLib
+
+import dbus
+import dbus.exceptions
+import dbus.mainloop.glib
+import dbus.service
+import time
+import threading
+
+ad_manager = None
+airplay_advertisement = None
+port = int(0)
+advmin = int(100)
+advmax = int(100)
+ipv4_str = "ipv4_address"
+index = int(0)
+
+BLUEZ_SERVICE_NAME = 'org.bluez'
+LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
+DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
+DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
+
+LE_ADVERTISEMENT_IFACE = 'org.bluez.LEAdvertisement1'
+
+
+class InvalidArgsException(dbus.exceptions.DBusException):
+ _dbus_error_name = 'org.freedesktop.DBus.Error.InvalidArgs'
+
+
+class NotSupportedException(dbus.exceptions.DBusException):
+ _dbus_error_name = 'org.bluez.Error.NotSupported'
+
+
+class NotPermittedException(dbus.exceptions.DBusException):
+ _dbus_error_name = 'org.bluez.Error.NotPermitted'
+
+
+class InvalidValueLengthException(dbus.exceptions.DBusException):
+ _dbus_error_name = 'org.bluez.Error.InvalidValueLength'
+
+
+class FailedException(dbus.exceptions.DBusException):
+ _dbus_error_name = 'org.bluez.Error.Failed'
+
+
+class AirPlay_Service_Discovery_Advertisement(dbus.service.Object):
+ PATH_BASE = '/org/bluez/airplay_service_discovery_advertisement'
+
+ def __init__(self, bus, index):
+ self.path = self.PATH_BASE + str(index)
+ self.bus = bus
+ self.manufacturer_data = None
+ self.min_intrvl = 0
+ self.max_intrvl = 0
+
+ dbus.service.Object.__init__(self, bus, self.path)
+
+ def get_properties(self):
+ properties = dict()
+ properties['Type'] = 'broadcast'
+ if self.manufacturer_data is not None:
+ properties['ManufacturerData'] = dbus.Dictionary(
+ self.manufacturer_data, signature='qv')
+ if self.min_intrvl > 0:
+ properties['MinInterval'] = dbus.UInt32(self.min_intrvl)
+ if self.max_intrvl > 0:
+ properties['MaxInterval'] = dbus.UInt32(self.max_intrvl)
+ return {LE_ADVERTISEMENT_IFACE: properties}
+
+ def get_path(self):
+ return dbus.ObjectPath(self.path)
+
+ def add_manufacturer_data(self, manuf_code, manuf_data):
+ if not self.manufacturer_data:
+ self.manufacturer_data = dbus.Dictionary({}, signature='qv')
+ self.manufacturer_data[manuf_code] = dbus.Array(manuf_data, signature='y')
+
+ def set_min_intrvl(self, min_intrvl):
+ if self.min_intrvl == 0:
+ self.min_intrvl = 100
+ self.min_intrvl = max(min_intrvl, 100)
+
+ def set_max_intrvl(self, max_intrvl):
+ if self.max_intrvl == 0:
+ self.max_intrvl = 100
+ self.max_intrvl = max(max_intrvl, 100)
+
+ @dbus.service.method(DBUS_PROP_IFACE,
+ in_signature='s',
+ out_signature='a{sv}')
+ def GetAll(self, interface):
+ if interface != LE_ADVERTISEMENT_IFACE:
+ raise InvalidArgsException()
+ return self.get_properties()[LE_ADVERTISEMENT_IFACE]
+
+ @dbus.service.method(LE_ADVERTISEMENT_IFACE,
+ in_signature='',
+ out_signature='')
+ def Release(self):
+ print('%s: Released!' % self.path)
+
+
+class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement):
+
+ def __init__(self, bus, index, ipv4_str, port, min_intrvl, max_intrvl):
+ AirPlay_Service_Discovery_Advertisement.__init__(self, bus, index)
+ assert port > 0
+ assert port <= 65535
+ mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30
+ import ipaddress
+ ipv4_address = ipaddress.ip_address(ipv4_str);
+ ipv4 = bytearray(ipv4_address.packed);
+ mfg_data.extend(ipv4)
+ port_bytes = port.to_bytes(2, 'big')
+ mfg_data.extend(port_bytes)
+ self.add_manufacturer_data(0x004c, mfg_data)
+ self.set_min_intrvl(min_intrvl)
+ self.set_max_intrvl(max_intrvl)
+
+
+def register_ad_cb():
+ global ipv4_str
+ global port
+ print(f'AirPlay Service_Discovery Advertisement ({ipv4_str}:{port}) registered')
+
+
+def register_ad_error_cb(error):
+ print('Failed to register advertisement: ' + str(error))
+ mainloop.quit()
+
+
+def find_adapter(bus):
+ remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'),
+ DBUS_OM_IFACE)
+ objects = remote_om.GetManagedObjects()
+
+ for o, props in objects.items():
+ if LE_ADVERTISING_MANAGER_IFACE in props:
+ return o
+
+ return None
+
+
+def setup_beacon(ipv4_str, port, advmin, advmax, index):
+ global ad_manager
+ global airplay_advertisement
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ bus = dbus.SystemBus()
+ adapter = find_adapter(bus)
+ if not adapter:
+ print('LEAdvertisingManager1 interface not found')
+ return
+ adapter_props = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter),
+ "org.freedesktop.DBus.Properties")
+
+ adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1))
+
+ ad_manager = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter),
+ LE_ADVERTISING_MANAGER_IFACE)
+ airplay_advertisement = AirPlayAdvertisement(bus, index, ipv4_str, port, advmin, advmax)
+
+def beacon_on():
+ global ad_manager
+ ad_manager.RegisterAdvertisement(airplay_advertisement.get_path(), {},
+ reply_handler=register_ad_cb,
+ error_handler=register_ad_error_cb)
+def beacon_off():
+ global ad_manager
+ global airplay_advertisement
+ ad_manager.UnregisterAdvertisement(airplay_advertisement)
+ print(f'AirPlay Service-Discovery beacon advertisement unregistered')
+ ad_manager = None
+ dbus.service.Object.remove_from_connection(airplay_advertisement)
+ airplay_advertisement = None
+
+#==generic code (non-dbus) below here =============
+
+
+# global variables
+beacon_is_running = False
+beacon_is_pending_on = False
+beacon_is_pending_off = False
+
+def start_beacon():
+ global beacon_is_running
+ global port
+ global ipv4_str
+ global advmin
+ global advmax
+ global index
+ setup_beacon(ipv4_str, port, advmin, advmax, index)
+ beacon_on()
+ beacon_is_running = True
+
+def stop_beacon():
+ global beacon_is_running
+ beacon_off()
+ beacon_is_running = False
+
+def check_process_name(pid, pname):
+ try:
+ process = psutil.Process(pid)
+ if process.name().find(pname,0) == 0:
+ return True
+ else:
+ return False
+ except psutil.NoSuchProcess:
+ return False
+
+def check_pending():
+ global beacon_is_running
+ global beacon_is_pending_on
+ global beacon_is_pending_off
+ if beacon_is_running:
+ #print(f"beacon running")
+ if beacon_is_pending_off:
+ stop_beacon()
+ beacon_is_pending_off = False
+ else:
+ #print(f"beacon not running")
+ if beacon_is_pending_on:
+ start_beacon()
+ beacon_is_pending_on = False
+ return True
+
+
+def check_file_exists(file_path):
+ global port
+ global beacon_is_running
+ global beacon_is_pending_on
+ global beacon_is_pending_off
+
+ if os.path.exists(file_path):
+ with open(file_path, 'rb') as file:
+ data = file.read(2)
+ port = struct.unpack('= min):
+ raise ValueError("AdvMax was smaller than AdvMin")
+ if not (max <= 10240):
+ raise ValueError("AdvMax was larger than 10240 msecs")
+
+
+def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
+ global ipv4_str
+ global advmin
+ global advmax
+ global index
+ ipv4_str = ipv4_str_in
+ advmin = advmin_in
+ advmax = advmax_in
+ index = index_in
+
+ try:
+ while True:
+ try:
+ check_adv_intrvl(advmin, advmax)
+ except ValueError as e:
+ print(f"Error: {e}")
+ raise SystemExit(1)
+
+ GLib.timeout_add_seconds(5, on_timeout, file_path)
+ GLib.timeout_add_seconds(1, check_pending)
+ mainloop = GLib.MainLoop()
+ mainloop.run()
+ except KeyboardInterrupt:
+ print(f"\nExiting ...")
+ sys.exit(0)
+
+
+
+if __name__ == '__main__':
+
+ # Create an ArgumentParser object
+ parser = argparse.ArgumentParser(
+ description='A program that runs an AirPlay service discovery BLE beacon.',
+ epilog='Example: python beacon.py --ipv4 "192.168.1.100" --path "/home/user/ble" --AdvMin 100 --AdvMax 100"'
+ )
+
+ home_dir = os.path.expanduser("~")
+ # Add arguments
+ parser.add_argument(
+ '--file',
+ type=str,
+ default= home_dir + "/.uxplay.beacon",
+ help='beacon startup file (optional): one entry (key, value) per line, e.g. --ipv4 192.168.1.100, (lines startng with with # are ignored)'
+ )
+
+ parser.add_argument(
+ '--path',
+ type=str,
+ default= home_dir + "/.uxplay.ble",
+ help='path to AirPlay server BLE beacon information file (default: ~/.uxplay.ble)).'
+ )
+ parser.add_argument(
+ '--ipv4',
+ type=str,
+ default='use gethostbyname',
+ help='ipv4 address of AirPlay server (default: use gethostbyname).'
+ )
+
+ parser.add_argument(
+ '--AdvMin',
+ type=str,
+ default="0",
+ help='The minimum Advertising Interval (>= 100) units=msec, default 100)'
+ )
+ parser.add_argument(
+ '--AdvMax',
+ type=str,
+ default="0",
+ help='The maximum Advertising Interval (>= AdvMin, <= 10240) units=msec, default 100)'
+ )
+
+ parser.add_argument(
+ '--index',
+ type=str,
+ default="0",
+ help='use index >= 0 to distinguish multiple AirPlay Service Discovery beacons, default 0)'
+ )
+
+ # Parse the command-line argunts
+ args = parser.parse_args()
+ ipv4_str = None
+ path = None
+ advmin = int(100)
+ advmax = int(100)
+ index = int(0)
+
+ if args.file:
+ print(f"Using config file: {args.file}")
+ if os.path.exists(args.file):
+ with open(args.file, 'r') as file:
+ for line in file:
+ stripped_line = line.strip()
+ if stripped_line.startswith('#'):
+ continue
+ parts = stripped_line.partition(" ")
+ part0 = parts[0]
+ part2 = parts[2]
+ key = part0.strip()
+ value = part2.strip()
+ if key == "--path":
+ path = value
+ elif key == "--ipv4":
+ ipv4_str = value
+ elif key == "--AdvMin":
+ if value.isdigit():
+ advmin = int(value)
+ else:
+ print(f"Invalid config file input (--AdvMin) {value} in {args.file}")
+ raise SystemExit(1)
+ elif key == "--AdvMax":
+ if value.isdigit():
+ advmax = int(value)
+ else:
+ print(f"Invalid config file input (--AdvMax) {value} in {args.file}")
+ raise SystemExit(1)
+ elif key == "--index":
+ if value.isdigit():
+ index = int(value)
+ else:
+ print(f"Invalid config file input (--index) {value} in {args.file}")
+ raise SystemExit(1)
+ else:
+ print(f"Unknown key \"{key}\" in config file {args.file}")
+ raise SystemExit(1)
+
+ if args.ipv4 == "use gethostbyname":
+ if (ipv4_str is None):
+ ipv4_str = socket.gethostbyname(socket.gethostname())
+ else:
+ ipv4_str = args.ipv4
+
+ if args.AdvMin != "0":
+ if args.AdvMin.isdigit():
+ advmin = int(args.AdvMin)
+ else:
+ print("Invalid input (AdvMin) {args.AdvMin}")
+ raise SystemExit(1)
+
+ if args.AdvMax != "0":
+ if args.AdvMax.isdigit():
+ advmax = int(args.AdvMax)
+ else:
+ print("Invalid input (AdvMin) {args.AdvMin}")
+ raise SystemExit(1)
+
+ if args.index != "0":
+ if args.index.isdigit():
+ index = int(args.index)
+ else:
+ print("Invalid input (AdvMin) {args.AdvMin}")
+ raise SystemExit(1)
+ if index < 0:
+ raise ValueError("index was negative (forbidden)")
+
+ print(f"AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}, advmin:advmax {advmin}:{advmax} index:{index}")
+ print(f"(Press Ctrl+C to exit)")
+ main(args.path, ipv4_str, advmin, advmax, index)
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d797d79..b1f8b63 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -75,6 +75,14 @@ install( FILES README.md README.txt README.html LICENSE DESTINATION ${CMAKE_INST
install( FILES lib/llhttp/LICENSE-MIT DESTINATION ${CMAKE_INSTALL_DOCDIR}/llhttp )
install( FILES uxplay.service DESTINATION ${CMAKE_INSTALL_DOCDIR}/systemd )
+if (DBUS_FOUND)
+install( FILES Bluetooth_LE_beacon/dbus/uxplay-beacon.py
+ DESTINATION bin
+ PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ)
+install( FILES uxplay-beacon.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
+endif()
+
+
# uninstall target
if(NOT TARGET uninstall)
configure_file(
diff --git a/README.html b/README.html
index 610a2de..5f0765e 100644
--- a/README.html
+++ b/README.html
@@ -15,12 +15,14 @@ Bluetooth LE “beacon”, (a USB 4.0 or later “dongle” can be used). See
instructions below. The beacon runs independently of UxPlay and
regularly broadcasts a Bluetooth LE (“Low Energy”) 46 byte packet
informing nearby iOS/macOS devices of the local IPv4 network address of
-the UxPlay server, and which TCP port to contact UxPlay on. Instructions
-for manually setting up such a beacon in Linux are given below. It is hoped
-that users will submit Pull Requests contributing scripts for automating
-beacon setup on all platforms. (Python may be an appropriate language
-choice)
+the UxPlay server, and which TCP port to contact UxPlay on. A python
+script (Python >=3.6) “uxplay-beacon.py”, to broadcast the
+Service-Discovery advertisement will be installed on systems with DBus
+support (Linux and *BSD, using Bluez for Bluetooth control): this does
+not require enhanced “root permissions” to run. A
+windows version of this script is also planned for the future.
+Instructions are given
+below.
NEW on github: option
-vrtp <rest-of-pipeline> bypasses rendering by
UxPlay, and instead transmits rtp packets of decrypted h264 or h265
@@ -1437,11 +1439,12 @@ to a file to n or less. To change the name audiodump,
use -admp [n] filename. Note that (unlike dumped video) the
dumped audio is currently only useful for debugging, as it is not
containerized to make it playable with standard audio players.
--ble filename. Enable Bluetooth beacon
-Service Discovery. The PID and process name of the UxPlay process is
-recorded in filename, which must be the full path to a
-writeable file. (This file is created when UxPlay starts and deleted
-when it stops.) See below for beacon setup
+-ble [filename]. Enable Bluetooth beacon
+Service Discovery. The port, PID and process name of the UxPlay process
+is recorded by default in ~/.uxplay.ble : (this file is
+created when UxPlay starts and deleted when it stops.) Optionally the
+file filename, which must be the full path to a writeable file
+can instead be used. See below for beacon setup
instructions.
-d [n] Enable debug output; optional argument n=1
suppresses audio/video packet data in debug output. Note: this does not
@@ -1451,191 +1454,65 @@ GST_DEBUG=2” before running uxplay. To see GStreamer information
messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
Bluetooth LE beacon setup
-To allow UxPlay to work with Bluetooth Low Energy (LE) Service
-Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous) service
-discovery, start it with the option
-“-ble <path-to-writeable-file>”, which at startup
-writes a data file containing the uxplay TCP port for receiving replies
-to the advertisement, plus the uxplay process ID and process name, and
-is deleted when uxplay terminates normally. This file is not
-used in the simple manual method for creating a beacon described
-below.
-Bluetooth LE Service discovery uses a “beacon” broadcasting a simple
-14-byte advertisement
-“0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY” where XX XX XX
-XX is an IPv4 internet address (and port YY YY) of the UxPlay host
-translated into hexadecimal octets. For example,
-“XX XX XX XX YY YY” = “C0 A8 01 FD 1B 58”
-means 192.168.1.253 port 0x1b58 (decimal value 7000). UxPlay must be
-able to receive messages on this TCP port at this address. The uxplay
-option “-p” sets up uxplay to listen on the default port
-7000 for these messages, as used in the example above. Otherwise the
-port in the beacon message should be the first (<n>)
-of the 3 open TCP ports specified with uxplay option
--p <n>. If the -p option is not used
-(which is only possible if there is no active firewall) the TCP port is
-selected at random, and its value must be taken from the beginning of
-the file written with the -ble option.
-The full translation of this message is that it has length 0D = 0x0d
-= 13 octets, and is a single “Advertising Protocol Data Unit” (PDU) of
-type “FF”, called “Manufacturer-Specific Data”, with
-“manufacturer code” “4C 00” = 0x004c = Apple (note the
-reversal of octet order when two octets are combined to make a two-byte
-unsigned short integer), and
-“09 08 13 30 XX XX XX XX YY YY” is the Apple-specific
-data.
-The Apple-specific data contains a single Apple Data Unit with Apple
-type = 09 (Airplay), Apple Data length 08 (0x08 = 8 octets) and Apple
-Data “13 30 XX XX XX XX YY YY” where 13 = 0001 0011 is
-Apple Flags, 30 is a seed (which will be ignored), XX XX XX XX is the
-IPv4 internet address and YY YY is the port. This is smaller than the
-“iBeacon” Apple Data Unit, which has Apple type 02 and Apple length 15
-(0x15 = 21 octets).
-In addition to creating the message, we need to set the “Advertising
-type” (ADV_NONCONN_IND) and “Advertising interval” range [AdvMin,
-AdvMax], where 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000
-= 10.24 sec (intervals are given in units of 0.625 msec as uint16_t
-unsigned short integers). Setting AdvMin = AdvMax fixes the interval;
-AdvMin < AdvMax allows the choice of the time of each advertising
-broadcast to be flexible within an allowed window to avoid clashing with
-other Bluetooth tasks. Keep the default choice to broadcast
-simultaneously on all three advertising channels, 37,38,39.
-An automated script to setup and start the beacon should use a
-high-level interface such as: (Linux) Bluez LEAdvertisingManager1
-(with an example)
-and (Windows 10/11) BluetoothLEAdvertisementPublisherClass
-(with an example).
-We invite submission of Pull Requests for working
-implementations!
-Until automated scripts are available, a simple Linux-only low-level
-manual method is given below, using the hcitool and
-hciconfig utilities which directly access the HCI stack,
-and need elevated privileges (use sudo). These utilities
-have been declared “deprecated” and “obsolete” by BlueZ developers: on
-Debian-based Linux “sudo apt install bluez” still provides
-hcitool, but on some other Linux distributions, it is split
-off from the main BlueZ package into an “extra” package with a name like
-“bluez-deprecated”. If we get the AirPlay beacon to work using the newer
-bluetoothctl or btmgmt utilities, these
-instructions will be updated.
-First verify that a Bluetooth HCI interface is available:
-$hcitool dev
-Devices:
- hci1 E8:EA:6A:7C:3F:CC
- hci0 08:BE:AC:40:A9:DC
-This shows two devices with their MAC addresses. You can use
-“hciconfig -a” to see which versions of Bluetooth they
-implement: we require Bluetooth v4.0 or later; you may need to use a
-cheap USB Bluetooth dongle if your system does not have it, or will not
-let you use it for LE (Low Energy) transmissions.
-Choose which interface to use (we will use hci0), and reset it.
-$ sudo hciconfig hci0 reset
-
-Step 1. Configure the beacon by sending a configure
-command 0x0006 to the Bluetooth LE stack 0x08. hcitool
-echoes the HCI command and the 4-byte “HCI Event” response. The only
-important part of the response is that the last byte is
-“00” (= “success”: other values are error codes):
-
-$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
-
-< HCI Command: ogf 0x08, ocf 0x0006, plen 15
- A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00
-> HCI Event: 0x0e plen 4
- 02 06 20 00
-
-The first “0xa0 0x00” sets AdvMin = 0x00a0 = 100 msec.
-The second “0xa0 0x00” sets AdvMax = 0x00a0 = 100 msec.
-Then “0x03” sets the Advertising Type to ADV_NONCONN_IND.
-The other non-zero entry (0x07 = 0000 0111) is the flag for using all
-three advertising channels.
-An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120
-(“0x20 0x01”).
-Step 2. Set the advertising message with HCI LE
-command 0x0008. For this command, hcitool requires a 32 octet message
-after sudo hcitool -i hci0 cmd 0x08 0x0008: The first octet
-is the length 0E = 0x0e = 14 of the “significant part” of the following
-31 octets, followed by the 14 octets of the advertisement, then padded
-with 17 zeroes to a total length of 32 octets. The example below sends
-an IPv4 address 192.168.1.253 as “0xc0 0xa8 0x01 0xfd” and
-the TCP port as 0x1b 0x58 (port 7000 = 0x1b58):
-$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0e 0x0d 0xff 0x4c 0x00 0x09 0x08 0x13 0x30 0xc0 0xa8 0x01 0xfd 0x1b 0x58 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
-< HCI Command: ogf 0x08, ocf 0x0008, plen 32
- 0E 0D FF 4C 00 09 08 13 30 C0 A8 01 FD 1B 58 00 00 00 00 00
- 00 00 00 00 00 00 00 00 00 00 00 00
-> HCI Event: 0x0e plen 4
- 01 08 20 00
-Step 3. Start the beacon with a 1-byte message
-“0x01” = “on”, sent with HCI LE command 0x000a = 10:
-$ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
-< HCI Command: ogf 0x08, ocf 0x000a, plen 1
- 01
-> HCI Event: 0x0e plen 4
- 02 0A 20 00
-The full length of the broadcasted beacon message is 46 bytes. To
-stop the beacon, use this command to send the 1-byte message
-“0x00” = “off”.
+The python>=3.6 script for running a Bluetooth-LE Service
+Discovery beacon is uxplay-beacon.py. Currently only a DBus version (for
+Linux and *BSD) is available, and it is only installed on systems which
+support DBus.
+If uxplay will be run with option “uxplay -ble” (so it
+writes data for the Bluetooth beacon in the default BLE data file
+~/.uxplay.ble), just run uxplay-beacon.py in a
+separate terminal. The python script will start Bluetooth LE
+Service-Discovery advertising when it detects that UxPlay is running by
+checking if the BLE data file exists, and stop when it no longer detects
+a running UxPlay plus this file (it will restart advertising if UxPlay
+later reappears). The script will remain active until stopped with
+Ctrl+C in its terminal window (or its terminal window is closed).
+The beacon script can be more finely controlled using five possible
+options: these can be given on the command line, or read from a
+configuration file ~/.uxplay.beacon, if it exists.
+Configuration file entries are like the command line forms, one per line
+(e.g., --ipv4 192.168.1.100). Lines commented out with an
+initial # are ignored. Command line options override the
+configuration file options.
-- For testing Bluetooth beacon Service Discovery on Linux, you will
-need to suppress the avahi-daemon which provides DNS-SD Service
-Discovery on UxPlay’s Host system (replace
mask and
-stop below by unmask and start to
-restore DNS-SD service):
+--file <config file> read beacon options from
+<config file> instead of
+~/.uxplay.beacon.
+--ipv4 <ipv4 address>. This option can be
+used to specify the ipv4 address at which the UxPlay server should be
+contacted by the client. If it is not given, an address will be obtained
+automatically using gethostbyname. Only ipv4 addresses are
+supported.
+--path <BLE data file>. This overrides the
+default choice of BLE data file (~/.uxplay.ble) that is
+monitored by the beacon script. This also requires that uxplay is run
+with option “uxplay -ble <BLE data file>”.
+--AdvMin x, --AdvMax y. These controls
+the interval between BLE advertisement broadcasts. This interval is in
+the range [x, y], given in units of msecs. Allowed ranges are 100 <=
+x <= y <= 10240. If AdvMin=AdvMax, the interval is fixed: if
+AdvMin < AdvMax it is chosen flexibly in this range to avoid
+interfering with other tasks the Bluetooth device is carrying out. The
+default values are AdvMin = AdvMax = 100. The advertisement is broadcast
+on all three Bluetooth LE advertising channels: 37,38,39.
+--index x (default x = 0, x >= 0). This should be
+used to distinguish between multiple simultaneous instances of
+uxplay-beacon.py that are running to support multiple instances of
+UxPlay. Each instance must have its own BLE Data file (just as each
+instance of UxPlay must also have its own MAC address and ports).
+Note: running multiple beacons simultaneously on the same host has
+not been tested.
+If you wish to test Bluetooth LE Service Discovery on Linux/*BSD, you
+can disable DNS_SD Service discovery by the avahi-daemon with
$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
-An automated procedure for creating the beacon would presumably want
-to switch it on when uxplay starts, and off when it stops. It has the
-task of determing a host IPv4 address that the client can use to reach
-uxplay. The 22-byte file created when uxplay starts (and deleted when it
-stops) contains the RAOP port as a uint16_t unsigned short, in the first
-2 bytes, followed by the uxplay PID as a uint32_t unsigned integer in
-the next 4 bytes, then followed by up to the first 15 characters of the
-process name (usually “uxplay”) as a null-terminated string, padded with
-zeroes to 16 bytes. The port data identifies the port on the Host that
-uxplay listens on, which should be included along with the Host IPv4
-address in the advertisement broadcast by the beacon. The path to this
-file is needed as the only input by the procedure when it is started.
-The presence of the file should be checked at regular intervals (once
-per second?). If it is absent, uxplay has stopped running, but if it
-exists the process ID and process name of that PID should be checked to
-handle cases where a new uxplay process has started, or if uxplay has
-exited abnormally and failed to delete the file. (While it is probably
-not an important use case, the possibility of concurrent uxplay
-processes listening on different ports and writing different files could
-be handled: the advertising protocol allows cycling between different
-messages.)
-This method above creates a beacon that identifies itself with a
-“public Advertising Address” (the MAC hardware address of the Bluetooth
-device). An Apple TV uses a private random address. If you wish to do
-that, change the sixth octet (the one following 0x03) in
-Step 1 from “TxAdd” = 0x00 to TxAdd = 0x01,
-and add an intermediate “step 1.5”:
-Step 1.5 Choose 6 random bytes r1, r2, r3, r4, r5,
-r6, such as “0x52 0xaa, 0xaa, 0x3a, 0xb4, 0x2f”, and use
-HCI LE command 0x0005 to set the random address:
-$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
-< HCI Command: ogf 0x08, ocf 0x0005, plen 6
- 52 AA AA 3A B4 2F
-> HCI Event: 0x0e plen 4
- 02 05 20 00
-On a Bluetooth packet sniffer with wireshark, this address displays
-as: Advertising Address: 2f:b4:3a:aa:aa:52. In
-principle, random byte r6 should be masked with 0x03 (r6 = r6 | 0x03) to
-mark the address as a “static random private address”, but Apple TV does
-not do this. In fact it updates to a new random Advertising Address
-every 20 mins or so, increasing the seed in the Apple Data by 1 each
-time. Apple TV’s also add a length 2 type 0x01 (“Flags”) Advertising PDU
-“0x02 0x01 0x1a” in front of the main type 0xff
-“Manufacturer-Specific Data” Advertising PDU in Step 2. This is
-“optional” for ADV_NONCONN_IND advertisement type, and testing shows
-that it can be dropped without affecting Service Discovery, which is
-fortunate because the high-level Linux and Windows interfaces mentioned
-earlier do not permit users to send a “Flags”-type PDU.
+To restore DNS_SD Service discovery, replace “mask” by “unmask”, and
+“stop” by “start”.
+For more information, see the wiki
+page This has useful information if you wish to build a python
+beacon controller script for Windows (we would like to have one!).
- Our current understanding is that Bluetooth LE AirPlay
Service Discovery only supports broadcast of IPv4 addresses. Please let
diff --git a/README.md b/README.md
index b478591..ce57702 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,11 @@
- **NEW on github**: Support for **service discovery using a Bluetooth LE "beacon"** (as an alternative to Bonjour/Rendezvous DNS-SD
service discovery). The user must set up a Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used). See instructions
below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet informing nearby iOS/macOS devices of
- the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on.
- Instructions for manually setting up such a beacon in Linux are [given below](#bluetooth-le-beacon-setup).
- __It is hoped that users will submit Pull Requests contributing scripts for automating beacon setup on all platforms.
- (Python may be an appropriate language choice)__
+ the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on. A python script (Python >=3.6) "uxplay-beacon.py",
+ to broadcast the Service-Discovery advertisement will be installed on systems with DBus support (Linux and *BSD, using Bluez for Bluetooth control):
+ this does **not** require enhanced "root permissions" to run.
+ A windows version of this script is also planned for the future.
+ Instructions are [given below](#bluetooth-le-beacon-setup).
- **NEW on github**: option `-vrtp ` bypasses rendering by UxPlay, and instead
transmits rtp packets of decrypted h264 or h265 video to
@@ -1452,10 +1453,13 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
-**-ble *filename***. Enable Bluetooth beacon Service Discovery.
-The PID and process name of the UxPlay process is recorded in
-*filename*, which must be the full path to a writeable file. (This file is created
-when UxPlay starts and deleted when it stops.) __See below for beacon setup
+**-ble [*filename*]**. Enable Bluetooth beacon Service Discovery.
+The port, PID and process name of the UxPlay process is recorded by default in
+`~/.uxplay.ble` : (this file is created
+when UxPlay starts and deleted when it stops.)
+Optionally the file
+*filename*, which must be the full path to a writeable file can instead be used.
+__See below for beacon setup
instructions.__
**-d \[n\]** Enable debug output; optional argument n=1 suppresses audio/video
@@ -1469,170 +1473,51 @@ GStreamer inner workings.
# Bluetooth LE beacon setup
-To allow UxPlay to work with Bluetooth Low Energy (LE) Service Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous)
-service discovery, start it with the option "`-ble `", which at startup writes a data file containing
-the uxplay TCP port for receiving replies to the advertisement, plus the uxplay process ID and process name, and is deleted when uxplay terminates normally. **This file
-is not used in the simple manual method for creating a beacon described below**.
+The python>=3.6 script for running a Bluetooth-LE Service Discovery beacon is uxplay-beacon.py.
+Currently only a DBus version (for Linux and *BSD) is available, and it is only installed on systems which
+support DBus.
+If uxplay will be run with option "`uxplay -ble`" (so it writes data for the Bluetooth beacon in the default BLE data file
+`~/.uxplay.ble`), just run ``uxplay-beacon.py`` in a separate terminal. The python script will start
+Bluetooth LE Service-Discovery advertising when it detects that UxPlay is running by checking if the BLE data file exists, and stop when it no longer detects
+a running UxPlay plus this file (it will restart advertising if UxPlay later reappears). The script will remain active until stopped with Ctrl+C in its
+terminal window (or its terminal window is closed).
-Bluetooth LE Service discovery uses a "beacon" broadcasting a simple 14-byte
-advertisement "`0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY`" where XX XX XX XX is an IPv4 internet
-address (and port YY YY) of the UxPlay host translated into hexadecimal octets. For
-example, "`XX XX XX XX YY YY`" = "``C0 A8 01 FD 1B 58``" means 192.168.1.253 port 0x1b58 (decimal value 7000). UxPlay
-must be able to receive messages on this TCP port at this
-address. The uxplay option "`-p`" sets up uxplay to listen on the default port 7000 for these messages, as used in the
-example above. Otherwise the port in the beacon message should
-be the first (``) of the 3 open TCP ports specified with uxplay option ``-p ``. If
-the `-p` option is not used (which is only possible if there is no active firewall) the TCP port is selected at random, and its value
-must be taken from the beginning of the file written with the `-ble` option.
+The beacon script can be more finely controlled using five possible options: these can be given on the command line, or read from
+a configuration file `~/.uxplay.beacon`, if it exists. Configuration file entries are like the command line forms, one per line (e.g.,
+`--ipv4 192.168.1.100`). Lines commented out with an initial ``#`` are ignored. Command line options override the configuration file
+options.
-The full translation of this message is that it has length 0D = 0x0d = 13 octets, and is a single "Advertising Protocol Data Unit" (PDU) of type "`FF`",
-called "Manufacturer-Specific Data", with "manufacturer code" "`4C 00`" = 0x004c = Apple (note the reversal of octet order when
-two octets are combined to make a two-byte unsigned short integer), and "`09 08 13 30 XX XX XX XX YY YY`" is the Apple-specific data.
+* `--file ` read beacon options from ```` instead of
+`~/.uxplay.beacon`.
-The Apple-specific data contains a single Apple Data Unit with Apple type = 09 (Airplay), Apple Data length 08 (0x08 = 8 octets) and
-Apple Data "`13 30 XX XX XX XX YY YY`" where 13 = 0001 0011 is Apple Flags, 30 is a seed (which will be ignored), XX XX XX XX
-is the IPv4 internet address and YY YY is the port. This is smaller than the "iBeacon" Apple Data Unit, which has Apple type 02 and Apple length 15 (0x15 = 21 octets).
+* `--ipv4 `. This option can be used to specify the ipv4 address at which the UxPlay server should be contacted by the client. If
+it is not given, an address will be obtained automatically using `gethostbyname`. Only ipv4 addresses are supported.
-In addition to creating the message, we need to set the "Advertising type" (ADV_NONCONN_IND) and "Advertising interval" range [AdvMin, AdvMax],
-where 0x00a0 = 100 msec <= AdvMin <= AdvMax <= 0x4000 = 10.24 sec
-(intervals are given in units of 0.625 msec as uint16_t unsigned short integers). Setting AdvMin = AdvMax fixes the interval; AdvMin < AdvMax allows the choice
-of the time of each advertising broadcast to be flexible within an allowed window to avoid clashing with other Bluetooth tasks.
-Keep the default choice to broadcast simultaneously on all three advertising channels, 37,38,39.
+* `--path `. This overrides the default choice of BLE data file (``~/.uxplay.ble``) that is monitored by the beacon script. This also requires
+that uxplay is run with option "`uxplay -ble `".
-An automated script to setup and start the beacon should use a high-level interface
-such as: (Linux) Bluez [LEAdvertisingManager1](https://manpages.opensuse.org/Leap-16.0/bluez/org.bluez.LEAdvertisement.5.en.html) (with
-an [example](https://github.com/bluez/bluez/blob/master/test/example-advertisement))
-and (Windows 10/11) [BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
-(with an [example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
-**We invite submission of Pull Requests for working implementations!**
+* `--AdvMin x`, ``--AdvMax y``. These controls the interval between BLE advertisement broadcasts. This interval is in the range
+[x, y], given in units of msecs. Allowed ranges are 100 <= x <= y <= 10240. If AdvMin=AdvMax, the interval is fixed: if AdvMin < AdvMax
+it is chosen flexibly in this range to avoid interfering with other tasks the Bluetooth device is carrying out. The default values are
+AdvMin = AdvMax = 100. The advertisement is broadcast on all three Bluetooth LE advertising channels: 37,38,39.
-Until automated scripts are available, a simple Linux-only low-level manual method
-is given below, using the `hcitool` and ``hciconfig``
-utilities which directly access the HCI stack, and need elevated privileges (use `sudo`). These utilities
-have been declared "deprecated" and "obsolete" by BlueZ developers: on Debian-based Linux "`sudo apt install bluez`"
-still provides `hcitool`, but on some other Linux distributions, it is split off from the main BlueZ package into an "extra" package
-with a name like "bluez-deprecated". If we get the AirPlay beacon to work using the newer `bluetoothctl` or ``btmgmt`` utilities,
-these instructions will be updated.
+* `--index x` (default x = 0, x >= 0). This should be used to distinguish between multiple simultaneous instances of uxplay-beacon.py that are running to support multiple
+ instances of UxPlay. Each instance must have its own BLE Data file (just as each instance of UxPlay must also have its own MAC address and ports). _Note:
+ running multiple beacons simultaneously on the same host has not been tested._
-First verify that a Bluetooth HCI interface is available:
-
-```
-$hcitool dev
-Devices:
- hci1 E8:EA:6A:7C:3F:CC
- hci0 08:BE:AC:40:A9:DC
-```
-
-This shows two devices with their MAC addresses. You can use "`hciconfig -a`" to see which versions of Bluetooth they
-implement: we require Bluetooth v4.0 or later;
-you may need to use a cheap USB Bluetooth dongle if your system does not have it,
-or will not let you use it for LE (Low Energy) transmissions.
-Choose which interface to use (we will use hci0), and reset it.
-
-```
-$ sudo hciconfig hci0 reset
-
-```
-
-**Step 1.** Configure the beacon by sending a configure command 0x0006 to the Bluetooth LE stack 0x08. `hcitool` echoes the HCI command
-and the 4-byte "HCI Event" response. The only important part of the response is that the last byte is "`00`" (= "success":
-other values are error codes):
-
-
-```
-
-$ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
-
-< HCI Command: ogf 0x08, ocf 0x0006, plen 15
- A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00
-> HCI Event: 0x0e plen 4
- 02 06 20 00
-
-```
-
-The first "`0xa0 0x00`" sets AdvMin = 0x00a0 = 100 msec. The second "``0xa0 0x00``" sets AdvMax = 0x00a0 = 100 msec.
-Then "`0x03`" sets the Advertising Type to ADV_NONCONN_IND. The other non-zero entry (0x07 = 0000 0111) is the flag for using
-all three
-advertising channels.
-
-An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120 ("`0x20 0x01`").
-
-**Step 2.** Set the advertising message with HCI LE command 0x0008. For this command, hcitool requires a 32 octet message after
-`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0E = 0x0e = 14 of the "significant part" of the following 31 octets,
-followed by the 14 octets of the advertisement, then padded with 17 zeroes to a total length of 32 octets. The example below sends an
-IPv4 address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`" and the TCP port as 0x1b 0x58 (port 7000 = 0x1b58):
-
-```
-$ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0e 0x0d 0xff 0x4c 0x00 0x09 0x08 0x13 0x30 0xc0 0xa8 0x01 0xfd 0x1b 0x58 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
-< HCI Command: ogf 0x08, ocf 0x0008, plen 32
- 0E 0D FF 4C 00 09 08 13 30 C0 A8 01 FD 1B 58 00 00 00 00 00
- 00 00 00 00 00 00 00 00 00 00 00 00
-> HCI Event: 0x0e plen 4
- 01 08 20 00
-```
-
-**Step 3**. Start the beacon with a 1-byte message "`0x01`" = "on", sent with HCI LE command 0x000a = 10:
-
-```
-$ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
-< HCI Command: ogf 0x08, ocf 0x000a, plen 1
- 01
-> HCI Event: 0x0e plen 4
- 02 0A 20 00
-```
-The full length of the broadcasted beacon message is 46 bytes.
-To stop the beacon, use this command to send the 1-byte message "`0x00`" = "off".
-
-
-
-* For testing Bluetooth beacon Service Discovery on Linux, you will need to suppress the avahi-daemon which
-provides DNS-SD Service Discovery on UxPlay's Host system (replace `mask` and ``stop`` below
-by `unmask` and ``start`` to restore DNS-SD service):
+If you wish to test Bluetooth LE Service Discovery on Linux/*BSD, you can disable DNS_SD Service discovery by the avahi-daemon with
```
$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
```
-An automated procedure for creating the beacon would presumably want to switch it on when uxplay starts, and off when it
-stops. It has the task of determing a host IPv4 address that the client can use to reach uxplay.
-The 22-byte file created when uxplay starts (and deleted when it stops) contains the RAOP port as a uint16_t unsigned short,
-in the first 2 bytes, followed by
-the uxplay PID as a uint32_t unsigned integer in the next 4 bytes, then
-followed by up to the first
-15 characters of the process name (usually "uxplay") as a null-terminated string, padded with zeroes to 16 bytes. The port data
-identifies the port on the Host that uxplay listens on, which should be included along with the Host IPv4 address
-in the advertisement broadcast by the beacon. The path to this file is needed as the only input by the procedure when it is started.
-The presence of the file should be checked at regular intervals (once per second?). If it is absent, uxplay has stopped running,
-but if it exists the process ID and process name of that PID should be checked to handle cases where a new uxplay process has
-started, or if uxplay has exited abnormally and failed to delete the file. (While it is probably not an important use case, the possibility of
-concurrent uxplay processes listening on different ports and writing different files could be handled: the advertising protocol allows
-cycling between different messages.)
+To restore DNS_SD Service discovery, replace "mask" by "unmask", and "stop" by "start".
-This method above creates a beacon that identifies itself with a "public Advertising Address" (the MAC hardware address of
-the Bluetooth device). An Apple TV uses a private random address. If you wish to do that, change the sixth octet (the one following `0x03`)
-in Step 1 from "TxAdd" = `0x00` to TxAdd = ``0x01``, and add an intermediate "step 1.5":
-
-**Step 1.5** Choose 6 random bytes r1, r2, r3, r4, r5, r6, such as
-"`0x52 0xaa, 0xaa, 0x3a, 0xb4, 0x2f`", and use HCI LE command 0x0005 to set the random address:
-
-```
-$sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
-< HCI Command: ogf 0x08, ocf 0x0005, plen 6
- 52 AA AA 3A B4 2F
-> HCI Event: 0x0e plen 4
- 02 05 20 00
-```
-
-On a Bluetooth packet sniffer with wireshark, this address displays as: **Advertising Address: 2f:b4:3a:aa:aa:52**.
-In principle, random byte r6 should be masked with 0x03 (r6 = r6 | 0x03) to mark the address as a "static random private address",
-but Apple TV does not do this. In fact it updates to a new random Advertising Address every 20 mins or so, increasing
-the seed in the Apple Data by 1 each time. Apple TV's also add a length 2 type 0x01 ("Flags") Advertising PDU "`0x02 0x01 0x1a`" in front of
-the main type 0xff "Manufacturer-Specific Data" Advertising PDU in Step 2. This is "optional" for ADV_NONCONN_IND advertisement type,
-and testing shows that it can be dropped without affecting Service Discovery, which is fortunate
-because the high-level Linux and Windows interfaces mentioned earlier do not permit users to send a "Flags"-type PDU.
+For more information, see the [wiki page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon)
+This has useful information if you wish to build a python beacon controller script for Windows (we would like to have one!).
* **Our current understanding is that Bluetooth LE AirPlay Service Discovery only supports
broadcast of IPv4 addresses. Please let us know if this is incorrect, or if IPv6 support is introduced in the future.**
diff --git a/README.txt b/README.txt
index 4667087..4f9292b 100644
--- a/README.txt
+++ b/README.txt
@@ -9,12 +9,13 @@
beacon runs independently of UxPlay and regularly broadcasts a
Bluetooth LE ("Low Energy") 46 byte packet informing nearby
iOS/macOS devices of the local IPv4 network address of the UxPlay
- server, and which TCP port to contact UxPlay on. Instructions for
- manually setting up such a beacon in Linux are [given
- below](#bluetooth-le-beacon-setup). **It is hoped that users will
- submit Pull Requests contributing scripts for automating beacon
- setup on all platforms. (Python may be an appropriate language
- choice)**
+ server, and which TCP port to contact UxPlay on. A python script
+ (Python \>=3.6) "uxplay-beacon.py", to broadcast the
+ Service-Discovery advertisement will be installed on systems with
+ DBus support (Linux and \*BSD, using Bluez for Bluetooth control):
+ this does **not** require enhanced "root permissions" to run. A
+ windows version of this script is also planned for the future.
+ Instructions are [given below](#bluetooth-le-beacon-setup).
- **NEW on github**: option `-vrtp ` bypasses
rendering by UxPlay, and instead transmits rtp packets of decrypted
@@ -1478,11 +1479,12 @@ that (unlike dumped video) the dumped audio is currently only useful for
debugging, as it is not containerized to make it playable with standard
audio players.*
-**-ble *filename***. Enable Bluetooth beacon Service Discovery. The PID
-and process name of the UxPlay process is recorded in *filename*, which
-must be the full path to a writeable file. (This file is created when
-UxPlay starts and deleted when it stops.) **See below for beacon setup
-instructions.**
+**-ble \[*filename*\]**. Enable Bluetooth beacon Service Discovery. The
+port, PID and process name of the UxPlay process is recorded by default
+in `~/.uxplay.ble` : (this file is created when UxPlay starts and
+deleted when it stops.) Optionally the file *filename*, which must be
+the full path to a writeable file can instead be used. **See below for
+beacon setup instructions.**
**-d \[n\]** Enable debug output; optional argument n=1 suppresses
audio/video packet data in debug output. Note: this does not show
@@ -1494,206 +1496,72 @@ this to see even more of the GStreamer inner workings.
# Bluetooth LE beacon setup
-To allow UxPlay to work with Bluetooth Low Energy (LE) Service
-Discovery, as an alternative to DNS-SD (Bonjour/Rendezvous) service
-discovery, start it with the option "`-ble `",
-which at startup writes a data file containing the uxplay TCP port for
-receiving replies to the advertisement, plus the uxplay process ID and
-process name, and is deleted when uxplay terminates normally. **This
-file is not used in the simple manual method for creating a beacon
-described below**.
+The python\>=3.6 script for running a Bluetooth-LE Service Discovery
+beacon is uxplay-beacon.py. Currently only a DBus version (for Linux and
+\*BSD) is available, and it is only installed on systems which support
+DBus.
-Bluetooth LE Service discovery uses a "beacon" broadcasting a simple
-14-byte advertisement "`0D FF 4C 00 09 08 13 30 XX XX XX XX YY YY`"
-where XX XX XX XX is an IPv4 internet address (and port YY YY) of the
-UxPlay host translated into hexadecimal octets. For example,
-"`XX XX XX XX YY YY`" = "`C0 A8 01 FD 1B 58`" means 192.168.1.253 port
-0x1b58 (decimal value 7000). UxPlay must be able to receive messages on
-this TCP port at this address. The uxplay option "`-p`" sets up uxplay
-to listen on the default port 7000 for these messages, as used in the
-example above. Otherwise the port in the beacon message should be the
-first (``) of the 3 open TCP ports specified with uxplay option
-`-p `. If the `-p` option is not used (which is only possible if
-there is no active firewall) the TCP port is selected at random, and its
-value must be taken from the beginning of the file written with the
-`-ble` option.
+If uxplay will be run with option "`uxplay -ble`" (so it writes data for
+the Bluetooth beacon in the default BLE data file `~/.uxplay.ble`), just
+run `uxplay-beacon.py` in a separate terminal. The python script will
+start Bluetooth LE Service-Discovery advertising when it detects that
+UxPlay is running by checking if the BLE data file exists, and stop when
+it no longer detects a running UxPlay plus this file (it will restart
+advertising if UxPlay later reappears). The script will remain active
+until stopped with Ctrl+C in its terminal window (or its terminal window
+is closed).
-The full translation of this message is that it has length 0D = 0x0d =
-13 octets, and is a single "Advertising Protocol Data Unit" (PDU) of
-type "`FF`", called "Manufacturer-Specific Data", with "manufacturer
-code" "`4C 00`" = 0x004c = Apple (note the reversal of octet order when
-two octets are combined to make a two-byte unsigned short integer), and
-"`09 08 13 30 XX XX XX XX YY YY`" is the Apple-specific data.
+The beacon script can be more finely controlled using five possible
+options: these can be given on the command line, or read from a
+configuration file `~/.uxplay.beacon`, if it exists. Configuration file
+entries are like the command line forms, one per line (e.g.,
+`--ipv4 192.168.1.100`). Lines commented out with an initial `#` are
+ignored. Command line options override the configuration file options.
-The Apple-specific data contains a single Apple Data Unit with Apple
-type = 09 (Airplay), Apple Data length 08 (0x08 = 8 octets) and Apple
-Data "`13 30 XX XX XX XX YY YY`" where 13 = 0001 0011 is Apple Flags, 30
-is a seed (which will be ignored), XX XX XX XX is the IPv4 internet
-address and YY YY is the port. This is smaller than the "iBeacon" Apple
-Data Unit, which has Apple type 02 and Apple length 15 (0x15 = 21
-octets).
+- `--file ` read beacon options from ``
+ instead of `~/.uxplay.beacon`.
-In addition to creating the message, we need to set the "Advertising
-type" (ADV_NONCONN_IND) and "Advertising interval" range \[AdvMin,
-AdvMax\], where 0x00a0 = 100 msec \<= AdvMin \<= AdvMax \<= 0x4000 =
-10.24 sec (intervals are given in units of 0.625 msec as uint16_t
-unsigned short integers). Setting AdvMin = AdvMax fixes the interval;
-AdvMin \< AdvMax allows the choice of the time of each advertising
-broadcast to be flexible within an allowed window to avoid clashing with
-other Bluetooth tasks. Keep the default choice to broadcast
-simultaneously on all three advertising channels, 37,38,39.
+- `--ipv4 `. This option can be used to specify the
+ ipv4 address at which the UxPlay server should be contacted by the
+ client. If it is not given, an address will be obtained
+ automatically using `gethostbyname`. Only ipv4 addresses are
+ supported.
-An automated script to setup and start the beacon should use a
-high-level interface such as: (Linux) Bluez
-[LEAdvertisingManager1](https://manpages.opensuse.org/Leap-16.0/bluez/org.bluez.LEAdvertisement.5.en.html)
-(with an
-[example](https://github.com/bluez/bluez/blob/master/test/example-advertisement))
-and (Windows 10/11)
-[BluetoothLEAdvertisementPublisherClass](https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher)
-(with an
-[example](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/uwp/devices-sensors/ble-beacon.md)).
-**We invite submission of Pull Requests for working implementations!**
+- `--path `. This overrides the default choice of BLE
+ data file (`~/.uxplay.ble`) that is monitored by the beacon script.
+ This also requires that uxplay is run with option
+ "`uxplay -ble `".
-Until automated scripts are available, a simple Linux-only low-level
-manual method is given below, using the `hcitool` and `hciconfig`
-utilities which directly access the HCI stack, and need elevated
-privileges (use `sudo`). These utilities have been declared "deprecated"
-and "obsolete" by BlueZ developers: on Debian-based Linux
-"`sudo apt install bluez`" still provides `hcitool`, but on some other
-Linux distributions, it is split off from the main BlueZ package into an
-"extra" package with a name like "bluez-deprecated". If we get the
-AirPlay beacon to work using the newer `bluetoothctl` or `btmgmt`
-utilities, these instructions will be updated.
+- `--AdvMin x`, `--AdvMax y`. These controls the interval between BLE
+ advertisement broadcasts. This interval is in the range \[x, y\],
+ given in units of msecs. Allowed ranges are 100 \<= x \<= y
+ \<= 10240. If AdvMin=AdvMax, the interval is fixed: if AdvMin \<
+ AdvMax it is chosen flexibly in this range to avoid interfering with
+ other tasks the Bluetooth device is carrying out. The default values
+ are AdvMin = AdvMax = 100. The advertisement is broadcast on all
+ three Bluetooth LE advertising channels: 37,38,39.
-First verify that a Bluetooth HCI interface is available:
+- `--index x` (default x = 0, x \>= 0). This should be used to
+ distinguish between multiple simultaneous instances of
+ uxplay-beacon.py that are running to support multiple instances of
+ UxPlay. Each instance must have its own BLE Data file (just as each
+ instance of UxPlay must also have its own MAC address and ports).
+ *Note: running multiple beacons simultaneously on the same host has
+ not been tested.*
- $hcitool dev
- Devices:
- hci1 E8:EA:6A:7C:3F:CC
- hci0 08:BE:AC:40:A9:DC
+If you wish to test Bluetooth LE Service Discovery on Linux/\*BSD, you
+can disable DNS_SD Service discovery by the avahi-daemon with
-This shows two devices with their MAC addresses. You can use
-"`hciconfig -a`" to see which versions of Bluetooth they implement: we
-require Bluetooth v4.0 or later; you may need to use a cheap USB
-Bluetooth dongle if your system does not have it, or will not let you
-use it for LE (Low Energy) transmissions.\
-Choose which interface to use (we will use hci0), and reset it.
-
- $ sudo hciconfig hci0 reset
-
-**Step 1.** Configure the beacon by sending a configure command 0x0006
-to the Bluetooth LE stack 0x08. `hcitool` echoes the HCI command and the
-4-byte "HCI Event" response. The only important part of the response is
-that the last byte is "`00`" (= "success": other values are error
-codes):
-
-
- $ sudo hcitool -i hci0 cmd 0x08 0x0006 0xa0 0x00 0xa0 0x00 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x07 0x00
-
- < HCI Command: ogf 0x08, ocf 0x0006, plen 15
- A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00
- > HCI Event: 0x0e plen 4
- 02 06 20 00
-
-The first "`0xa0 0x00`" sets AdvMin = 0x00a0 = 100 msec. The second
-"`0xa0 0x00`" sets AdvMax = 0x00a0 = 100 msec. Then "`0x03`" sets the
-Advertising Type to ADV_NONCONN_IND. The other non-zero entry (0x07 =
-0000 0111) is the flag for using all three advertising channels.
-
-An Apple TV (Gen 3) seems to use AdvMin = AdvMax = 180 msec = 0x0120
-("`0x20 0x01`").
-
-**Step 2.** Set the advertising message with HCI LE command 0x0008. For
-this command, hcitool requires a 32 octet message after
-`sudo hcitool -i hci0 cmd 0x08 0x0008`: The first octet is the length 0E
-= 0x0e = 14 of the "significant part" of the following 31 octets,
-followed by the 14 octets of the advertisement, then padded with 17
-zeroes to a total length of 32 octets. The example below sends an IPv4
-address 192.168.1.253 as "`0xc0 0xa8 0x01 0xfd`" and the TCP port as
-0x1b 0x58 (port 7000 = 0x1b58):
-
- $ sudo hcitool -i hci0 cmd 0x08 0x0008 0x0e 0x0d 0xff 0x4c 0x00 0x09 0x08 0x13 0x30 0xc0 0xa8 0x01 0xfd 0x1b 0x58 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- < HCI Command: ogf 0x08, ocf 0x0008, plen 32
- 0E 0D FF 4C 00 09 08 13 30 C0 A8 01 FD 1B 58 00 00 00 00 00
- 00 00 00 00 00 00 00 00 00 00 00 00
- > HCI Event: 0x0e plen 4
- 01 08 20 00
-
-**Step 3**. Start the beacon with a 1-byte message "`0x01`" = "on", sent
-with HCI LE command 0x000a = 10:
-
- $ sudo hcitool -i hci0 cmd 0x08 0x000a 0x01
- < HCI Command: ogf 0x08, ocf 0x000a, plen 1
- 01
- > HCI Event: 0x0e plen 4
- 02 0A 20 00
-
-The full length of the broadcasted beacon message is 46 bytes. To stop
-the beacon, use this command to send the 1-byte message "`0x00`" =
-"off".
-
-- For testing Bluetooth beacon Service Discovery on Linux, you will
- need to suppress the avahi-daemon which provides DNS-SD Service
- Discovery on UxPlay's Host system (replace `mask` and `stop` below
- by `unmask` and `start` to restore DNS-SD service):
-
-```{=html}
-
-```
$ sudo systemctl mask avahi-daemon.socket
$ sudo systemctl stop avahi-daemon
-An automated procedure for creating the beacon would presumably want to
-switch it on when uxplay starts, and off when it stops. It has the task
-of determing a host IPv4 address that the client can use to reach
-uxplay. The 22-byte file created when uxplay starts (and deleted when it
-stops) contains the RAOP port as a uint16_t unsigned short, in the first
-2 bytes, followed by the uxplay PID as a uint32_t unsigned integer in
-the next 4 bytes, then followed by up to the first 15 characters of the
-process name (usually "uxplay") as a null-terminated string, padded with
-zeroes to 16 bytes. The port data identifies the port on the Host that
-uxplay listens on, which should be included along with the Host IPv4
-address in the advertisement broadcast by the beacon. The path to this
-file is needed as the only input by the procedure when it is started.
-The presence of the file should be checked at regular intervals (once
-per second?). If it is absent, uxplay has stopped running, but if it
-exists the process ID and process name of that PID should be checked to
-handle cases where a new uxplay process has started, or if uxplay has
-exited abnormally and failed to delete the file. (While it is probably
-not an important use case, the possibility of concurrent uxplay
-processes listening on different ports and writing different files could
-be handled: the advertising protocol allows cycling between different
-messages.)
+To restore DNS_SD Service discovery, replace "mask" by "unmask", and
+"stop" by "start".
-This method above creates a beacon that identifies itself with a "public
-Advertising Address" (the MAC hardware address of the Bluetooth device).
-An Apple TV uses a private random address. If you wish to do that,
-change the sixth octet (the one following `0x03`) in Step 1 from "TxAdd"
-= `0x00` to TxAdd = `0x01`, and add an intermediate "step 1.5":
-
-**Step 1.5** Choose 6 random bytes r1, r2, r3, r4, r5, r6, such as
-"`0x52 0xaa, 0xaa, 0x3a, 0xb4, 0x2f`", and use HCI LE command 0x0005 to
-set the random address:
-
- $sudo hcitool -i hci0 cmd 0x08 0x0005 0x52 0xaa 0xaa 0x3a 0xb4 0x2f
- < HCI Command: ogf 0x08, ocf 0x0005, plen 6
- 52 AA AA 3A B4 2F
- > HCI Event: 0x0e plen 4
- 02 05 20 00
-
-On a Bluetooth packet sniffer with wireshark, this address displays as:
-**Advertising Address: 2f:b4:3a:aa:aa:52**. In principle, random byte r6
-should be masked with 0x03 (r6 = r6 \| 0x03) to mark the address as a
-"static random private address", but Apple TV does not do this. In fact
-it updates to a new random Advertising Address every 20 mins or so,
-increasing the seed in the Apple Data by 1 each time. Apple TV's also
-add a length 2 type 0x01 ("Flags") Advertising PDU "`0x02 0x01 0x1a`" in
-front of the main type 0xff "Manufacturer-Specific Data" Advertising PDU
-in Step 2. This is "optional" for ADV_NONCONN_IND advertisement type,
-and testing shows that it can be dropped without affecting Service
-Discovery, which is fortunate because the high-level Linux and Windows
-interfaces mentioned earlier do not permit users to send a "Flags"-type
-PDU.
+For more information, see the [wiki
+page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon) This has
+useful information if you wish to build a python beacon controller
+script for Windows (we would like to have one!).
- **Our current understanding is that Bluetooth LE AirPlay Service
Discovery only supports broadcast of IPv4 addresses. Please let us
diff --git a/uxplay-beacon.1 b/uxplay-beacon.1
new file mode 100644
index 0000000..edfa117
--- /dev/null
+++ b/uxplay-beacon.1
@@ -0,0 +1,45 @@
+.TH UXPLAY 1 2025-10-26 "UxPlay 1.72" "User Commands"
+.SH NAME
+uxplay-beacon.py \- Python (>= 3.6) script for a Bluetooth LE Service-Discovery beacon.
+.SH SYNOPSIS
+.B uxplay-beacon.py
+[\fI\, -h, --help] + more options.
+.SH DESCRIPTION
+UxPlay 1.72: Standalone Python Script for managing Bluetooth LE Service Discovery.
+.SH OPTIONS
+.TP
+.B
+\fB\--file\fR fn Specify alternate configuration file
+.TP
+\fB\--path\fR fn Specify non-default Bluetooth LE data file used by uxplay
+.TP
+\fB\--ipv4\fR ip Override automatically-found ipv4 address for contacting UxPlay
+.TP
+\fB\--AdvMin\fR x Minimum Advertising interval in msecs (>= 100)
+.TP
+\fB\--AdvMin\fR y Maximum Advertising interval in msecs (>= AdvMin, <= 102400)
+.TP
+\fB\--index\fR x Used to distinguish different instances of beacons
+.TP
+\fB \-h, --help\fR Show help text.
+.SH
+FILES
+Options in beacon configuration file ~/.uxplay.beacon
+.TP
+are applied first (command-line options may modify them). Format:
+.TP
+one option per line, with initial "--"; lines beginning with "#" ignored.
+.SH
+AUTHORS
+.TP
+Various, see website or distribution.
+.SH
+COPYRIGHT
+.TP
+Various, see website or distribution. License: GPL v3+:
+.TP
+GNU GPL version 3 or later. (some parts LGPL v.2.1+ or MIT).
+.SH
+SEE ALSO
+.TP
+Website:
diff --git a/uxplay.1 b/uxplay.1
index 7a1fde2..4d2e715 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -1,4 +1,4 @@
-.TH UXPLAY "1" "May 2025" "1.72" "User Commands"
+.TH UXPLAY "1" "2025-10-26" "UxPlay 1.72" "User Commands"
.SH NAME
uxplay \- start AirPlay server
.SH SYNOPSIS
@@ -191,7 +191,9 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
audio packets are dumped. "aud"= unknown format.
.PP
.TP
-\fB\-ble\fI fn\fR For BluetoothLE beacon: write PID to file fn ("off" to cancel)
+\fB\-ble\fI [fn]\fR For BluetoothLE beacon: write data to default file ~/.uxplay.ble
+.IP
+ optional: write to file "fn" ("fn" = "off" to cancel)
.TP
\fB\-d [n]\fR Enable debug logging; optional: n=1 to skip normal packet data.
.TP
diff --git a/uxplay.cpp b/uxplay.cpp
index 2f06242..49ef86d 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -924,7 +924,8 @@ static void print_info (char *name) {
printf(" =1,2,..; fn=\"audiodump\"; change with \"-admp [n] filename\".\n");
printf(" x increases when audio format changes. If n is given, <= n\n");
printf(" audio packets are dumped. \"aud\"= unknown format.\n");
- printf("-ble fn For BluetoothLE beacon: write PID to file fn (\"off\" to cancel)\n");
+ printf("-ble [fn] For BluetoothLE beacon: write data to file ~/.uxplay.ble\n");
+ printf(" optional: write to file \"fn\" (\"fn\" = \"off\" to cancel)\n");
printf("-d [n] Enable debug logging; optional: n=1 to skip normal packet data\n");
printf("-v Displays version information\n");
printf("-h Displays this help\n");
@@ -1386,8 +1387,8 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
}
} else if (arg == "-ble" ) {
- if (option_has_value(i, argc, arg, argv[i+1])) {
- ble_filename.erase();
+ ble_filename.erase();
+ if (i < argc - 1 && *argv[i+1] != '-') {
i++;
if (strlen(argv[i]) != 3 || strncmp(argv[i], "off", 3)) {
ble_filename.append(argv[i]);
@@ -1397,8 +1398,18 @@ static void parse_arguments (int argc, char *argv[]) {
}
}
} else {
- fprintf(stderr,"option -ble must be followed by a filename for PID data or by \"off\"\n");
- exit(1);
+ static const char* homedir = get_homedir();
+ if (homedir) {
+ ble_filename = homedir;
+ ble_filename.append("/.uxplay.ble");
+ if (!file_has_write_access(ble_filename.c_str())) {
+ fprintf(stderr, "%s cannot be written to\n",ble_filename.c_str()) ;
+ exit(1);
+ }
+ } else {
+ fprintf(stderr,"failed to obtain home directory\n");
+ exit(1);
+ }
}
} else if (arg == "-bt709") {
bt709_fix = true;
From 50e74f25d169e43566e670b449c44b2f04ae6931 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Tue, 28 Oct 2025 20:19:58 -0400
Subject: [PATCH 47/58] uxplay-beacon.py: code cleanups
---
Bluetooth_LE_beacon/dbus/uxplay-beacon.py | 105 ++++++++++++----------
README.html | 12 ++-
README.md | 14 ++-
README.txt | 14 ++-
4 files changed, 93 insertions(+), 52 deletions(-)
diff --git a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
index 8c4e863..b73b073 100644
--- a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
@@ -5,30 +5,21 @@
# a standalone python-3.6 or later DBus-based AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
# (c) F. Duncanh, October 2025
-import argparse
import gi
-import os
-import sys
-import psutil
-import struct
-import socket
-
-from gi.repository import GLib
+try:
+ from gi.repository import GLib
+except ImportError:
+ print(f"ImportError: failed to import GLib")
+
import dbus
import dbus.exceptions
import dbus.mainloop.glib
import dbus.service
-import time
-import threading
ad_manager = None
airplay_advertisement = None
-port = int(0)
-advmin = int(100)
-advmax = int(100)
-ipv4_str = "ipv4_address"
-index = int(0)
+server_address = None
BLUEZ_SERVICE_NAME = 'org.bluez'
LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
@@ -112,7 +103,7 @@ class AirPlay_Service_Discovery_Advertisement(dbus.service.Object):
in_signature='',
out_signature='')
def Release(self):
- print('%s: Released!' % self.path)
+ print(f'{self.path}: Released!')
class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement):
@@ -134,14 +125,14 @@ class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement):
def register_ad_cb():
- global ipv4_str
- global port
- print(f'AirPlay Service_Discovery Advertisement ({ipv4_str}:{port}) registered')
+ global server_address
+ print(f'AirPlay Service_Discovery Advertisement ({server_address}) registered')
def register_ad_error_cb(error):
- print('Failed to register advertisement: ' + str(error))
- mainloop.quit()
+ print(f'Failed to register advertisement: {error}')
+ global ad_manager
+ ad_manager = None
def find_adapter(bus):
@@ -159,11 +150,13 @@ def find_adapter(bus):
def setup_beacon(ipv4_str, port, advmin, advmax, index):
global ad_manager
global airplay_advertisement
+ global server_address
+ server_address = f"{ipv4_str}:{port}"
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
adapter = find_adapter(bus)
if not adapter:
- print('LEAdvertisingManager1 interface not found')
+ print(f'LEAdvertisingManager1 interface not found')
return
adapter_props = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter),
"org.freedesktop.DBus.Properties")
@@ -176,9 +169,16 @@ def setup_beacon(ipv4_str, port, advmin, advmax, index):
def beacon_on():
global ad_manager
+ global airplay_advertisement
ad_manager.RegisterAdvertisement(airplay_advertisement.get_path(), {},
reply_handler=register_ad_cb,
error_handler=register_ad_error_cb)
+ if ad_manager is None:
+ airplay_advertisement = None
+ return False
+ else:
+ return True
+
def beacon_off():
global ad_manager
global airplay_advertisement
@@ -190,12 +190,24 @@ def beacon_off():
#==generic code (non-dbus) below here =============
+import argparse
+import os
+import sys
+import psutil
+import struct
+import socket
# global variables
beacon_is_running = False
beacon_is_pending_on = False
beacon_is_pending_off = False
+port = int(0)
+advmin = int(100)
+advmax = int(100)
+ipv4_str = "ipv4_address"
+index = int(0)
+
def start_beacon():
global beacon_is_running
global port
@@ -204,8 +216,7 @@ def start_beacon():
global advmax
global index
setup_beacon(ipv4_str, port, advmin, advmax, index)
- beacon_on()
- beacon_is_running = True
+ beacon_is_running = beacon_on()
def stop_beacon():
global beacon_is_running
@@ -227,12 +238,10 @@ def check_pending():
global beacon_is_pending_on
global beacon_is_pending_off
if beacon_is_running:
- #print(f"beacon running")
if beacon_is_pending_off:
stop_beacon()
beacon_is_pending_off = False
else:
- #print(f"beacon not running")
if beacon_is_pending_on:
start_beacon()
beacon_is_pending_on = False
@@ -259,12 +268,12 @@ def check_file_exists(file_path):
if not beacon_is_running:
beacon_is_pending_on = True
else:
- print(f"orphan beacon file {file_path} exists, but process {pname} (pid {pid}) is no longer active")
+ print(f'orphan beacon file {file_path} exists, but process {pname} (pid {pid}) is no longer active')
try:
os.remove(file_path)
- print(f"File '{file_path}' deleted successfully.")
+ print(f'File "{file_path}" deleted successfully.')
except FileNotFoundError:
- print(f"File '{file_path}' not found.")
+ print(f'File "{file_path}" not found.')
if beacon_is_running:
beacon_is_pending_off = True
else:
@@ -281,7 +290,7 @@ def process_input(value):
my_integer = int(value)
return my_integer
except ValueError:
- printf(f"Error: could not convert '{value}' to integer: {my_integer}")
+ print(f'Error: could not convert "{value}" to integer: {my_integer}')
return None
@@ -289,11 +298,11 @@ def process_input(value):
#check AdvInterval
def check_adv_intrvl(min, max):
if not (100 <= min):
- raise ValueError("AdvMin was smaller than 100 msecs")
+ raise ValueError('AdvMin was smaller than 100 msecs')
if not (max >= min):
- raise ValueError("AdvMax was smaller than AdvMin")
+ raise ValueError('AdvMax was smaller than AdvMin')
if not (max <= 10240):
- raise ValueError("AdvMax was larger than 10240 msecs")
+ raise ValueError('AdvMax was larger than 10240 msecs')
def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
@@ -311,7 +320,7 @@ def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
try:
check_adv_intrvl(advmin, advmax)
except ValueError as e:
- print(f"Error: {e}")
+ print(f'Error: {e}')
raise SystemExit(1)
GLib.timeout_add_seconds(5, on_timeout, file_path)
@@ -319,12 +328,16 @@ def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
mainloop = GLib.MainLoop()
mainloop.run()
except KeyboardInterrupt:
- print(f"\nExiting ...")
+ print(f'\nExiting ...')
sys.exit(0)
if __name__ == '__main__':
+
+
+ if not sys.version_info >= (3,6):
+ print("uxplay-beacon.py requires Python 3.6 or higher")
# Create an ArgumentParser object
parser = argparse.ArgumentParser(
@@ -383,7 +396,7 @@ if __name__ == '__main__':
index = int(0)
if args.file:
- print(f"Using config file: {args.file}")
+ print(f'Using config file: {args.file}')
if os.path.exists(args.file):
with open(args.file, 'r') as file:
for line in file:
@@ -403,22 +416,22 @@ if __name__ == '__main__':
if value.isdigit():
advmin = int(value)
else:
- print(f"Invalid config file input (--AdvMin) {value} in {args.file}")
+ print(f'Invalid config file input (--AdvMin) {value} in {args.file}')
raise SystemExit(1)
elif key == "--AdvMax":
if value.isdigit():
advmax = int(value)
else:
- print(f"Invalid config file input (--AdvMax) {value} in {args.file}")
+ print(f'Invalid config file input (--AdvMax) {value} in {args.file}')
raise SystemExit(1)
elif key == "--index":
if value.isdigit():
index = int(value)
else:
- print(f"Invalid config file input (--index) {value} in {args.file}")
+ print(f'Invalid config file input (--index) {value} in {args.file}')
raise SystemExit(1)
else:
- print(f"Unknown key \"{key}\" in config file {args.file}")
+ print(f'Unknown key "{key}" in config file {args.file}')
raise SystemExit(1)
if args.ipv4 == "use gethostbyname":
@@ -431,26 +444,26 @@ if __name__ == '__main__':
if args.AdvMin.isdigit():
advmin = int(args.AdvMin)
else:
- print("Invalid input (AdvMin) {args.AdvMin}")
+ print(f'Invalid input (AdvMin) {args.AdvMin}')
raise SystemExit(1)
if args.AdvMax != "0":
if args.AdvMax.isdigit():
advmax = int(args.AdvMax)
else:
- print("Invalid input (AdvMin) {args.AdvMin}")
+ print(f'Invalid input (AdvMin) {args.AdvMin}')
raise SystemExit(1)
if args.index != "0":
if args.index.isdigit():
index = int(args.index)
else:
- print("Invalid input (AdvMin) {args.AdvMin}")
+ print(f'Invalid input (AdvMin) {args.AdvMin}')
raise SystemExit(1)
if index < 0:
- raise ValueError("index was negative (forbidden)")
+ raise ValueError('index was negative (forbidden)')
- print(f"AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}, advmin:advmax {advmin}:{advmax} index:{index}")
- print(f"(Press Ctrl+C to exit)")
+ print(f'AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}, advmin:advmax {advmin}:{advmax} index:{index}')
+ print(f'(Press Ctrl+C to exit)')
main(args.path, ipv4_str, advmin, advmax, index)
diff --git a/README.html b/README.html
index 5f0765e..ceb564b 100644
--- a/README.html
+++ b/README.html
@@ -18,7 +18,7 @@ informing nearby iOS/macOS devices of the local IPv4 network address of
the UxPlay server, and which TCP port to contact UxPlay on. A python
script (Python >=3.6) “uxplay-beacon.py”, to broadcast the
Service-Discovery advertisement will be installed on systems with DBus
-support (Linux and *BSD, using Bluez for Bluetooth control): this does
+support (Linux and *BSD, using BlueZ for Bluetooth control): this does
not require enhanced “root permissions” to run. A
windows version of this script is also planned for the future.
Instructions are given
@@ -1457,7 +1457,15 @@ this to see even more of the GStreamer inner workings.
The python>=3.6 script for running a Bluetooth-LE Service
Discovery beacon is uxplay-beacon.py. Currently only a DBus version (for
Linux and *BSD) is available, and it is only installed on systems which
-support DBus.
+support DBus. Bluetooth >= 4.0 hardware on the host computer is
+required: a cheap USB bluetooth dongle can be used. Bluetooth support
+(BlueZ) must be installed (on Debian-based systems:
+sudo apt install bluez bluez-tools; recent Ubuntu releases
+provide bluez as a snap package).
+In addition to standard Python3 libraries, you may need to install
+the gi, dbus, and psutil Python libraries used by uxplay-beacon.py. On
+Debian-based systems:
+sudo apt install python3-gi python3-dbus python3-psutil
If uxplay will be run with option “uxplay -ble” (so it
writes data for the Bluetooth beacon in the default BLE data file
~/.uxplay.ble), just run uxplay-beacon.py in a
diff --git a/README.md b/README.md
index ce57702..064e077 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
service discovery). The user must set up a Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used). See instructions
below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet informing nearby iOS/macOS devices of
the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on. A python script (Python >=3.6) "uxplay-beacon.py",
- to broadcast the Service-Discovery advertisement will be installed on systems with DBus support (Linux and *BSD, using Bluez for Bluetooth control):
+ to broadcast the Service-Discovery advertisement will be installed on systems with DBus support (Linux and *BSD, using BlueZ for Bluetooth control):
this does **not** require enhanced "root permissions" to run.
A windows version of this script is also planned for the future.
Instructions are [given below](#bluetooth-le-beacon-setup).
@@ -1475,7 +1475,17 @@ GStreamer inner workings.
The python>=3.6 script for running a Bluetooth-LE Service Discovery beacon is uxplay-beacon.py.
Currently only a DBus version (for Linux and *BSD) is available, and it is only installed on systems which
-support DBus.
+support DBus. Bluetooth >= 4.0 hardware on the host computer is required: a cheap USB bluetooth dongle
+can be used. Bluetooth support (BlueZ) must be installed (on Debian-based systems: `sudo apt install bluez bluez-tools`;
+recent Ubuntu releases provide bluez as a snap package).
+
+In addition to standard Python3 libraries, you may need to install the gi, dbus, and psutil Python libraries used by
+uxplay-beacon.py. On Debian-based systems:
+
+```
+sudo apt install python3-gi python3-dbus python3-psutil
+```
+
If uxplay will be run with option "`uxplay -ble`" (so it writes data for the Bluetooth beacon in the default BLE data file
`~/.uxplay.ble`), just run ``uxplay-beacon.py`` in a separate terminal. The python script will start
diff --git a/README.txt b/README.txt
index 4f9292b..cc63607 100644
--- a/README.txt
+++ b/README.txt
@@ -12,7 +12,7 @@
server, and which TCP port to contact UxPlay on. A python script
(Python \>=3.6) "uxplay-beacon.py", to broadcast the
Service-Discovery advertisement will be installed on systems with
- DBus support (Linux and \*BSD, using Bluez for Bluetooth control):
+ DBus support (Linux and \*BSD, using BlueZ for Bluetooth control):
this does **not** require enhanced "root permissions" to run. A
windows version of this script is also planned for the future.
Instructions are [given below](#bluetooth-le-beacon-setup).
@@ -1499,7 +1499,17 @@ this to see even more of the GStreamer inner workings.
The python\>=3.6 script for running a Bluetooth-LE Service Discovery
beacon is uxplay-beacon.py. Currently only a DBus version (for Linux and
\*BSD) is available, and it is only installed on systems which support
-DBus.
+DBus. Bluetooth \>= 4.0 hardware on the host computer is required: a
+cheap USB bluetooth dongle can be used. Bluetooth support (BlueZ) must
+be installed (on Debian-based systems:
+`sudo apt install bluez bluez-tools`; recent Ubuntu releases provide
+bluez as a snap package).
+
+In addition to standard Python3 libraries, you may need to install the
+gi, dbus, and psutil Python libraries used by uxplay-beacon.py. On
+Debian-based systems:
+
+ sudo apt install python3-gi python3-dbus python3-psutil
If uxplay will be run with option "`uxplay -ble`" (so it writes data for
the Bluetooth beacon in the default BLE data file `~/.uxplay.ble`), just
From c961a4394abe9b74cab44ca3c887de11fb8648a9 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Thu, 30 Oct 2025 12:50:44 -0400
Subject: [PATCH 48/58] verify all options input is valid ascii/UTF-8
---
README.html | 3 ++-
README.md | 1 +
README.txt | 1 +
uxplay.1 | 2 +-
uxplay.cpp | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++--
5 files changed, 75 insertions(+), 4 deletions(-)
diff --git a/README.html b/README.html
index ceb564b..9bd7620 100644
--- a/README.html
+++ b/README.html
@@ -1060,7 +1060,8 @@ startup file location: this overrides $UXPLAYRC,
server_name@_hostname_ will be the name that appears offering AirPlay
services to your iPad, iPhone etc, where hostname is the name
of the server running uxplay. This will also now be the name shown above
-the mirror display (X11) window.
+the mirror display (X11) window. Internationalized server names
+encoded as UTF-8 are accepted.
-nh Do not append “@_hostname_” at the end of the AirPlay
server name.
diff --git a/README.md b/README.md
index 064e077..a0ff8a6 100644
--- a/README.md
+++ b/README.md
@@ -1044,6 +1044,7 @@ overrides `$UXPLAYRC`, ``~/.uxplayrc``, etc.
the name that appears offering AirPlay services to your iPad, iPhone
etc, where *hostname* is the name of the server running uxplay. This
will also now be the name shown above the mirror display (X11) window.
+**Internationalized server names encoded as UTF-8 are accepted.**
**-nh** Do not append "@_hostname_" at the end of the AirPlay server
name.
diff --git a/README.txt b/README.txt
index cc63607..fc3852b 100644
--- a/README.txt
+++ b/README.txt
@@ -1075,6 +1075,7 @@ this overrides `$UXPLAYRC`, `~/.uxplayrc`, etc.
the name that appears offering AirPlay services to your iPad, iPhone
etc, where *hostname* is the name of the server running uxplay. This
will also now be the name shown above the mirror display (X11) window.
+**Internationalized server names encoded as UTF-8 are accepted.**
**-nh** Do not append "@_hostname_" at the end of the AirPlay server
name.
diff --git a/uxplay.1 b/uxplay.1
index 4d2e715..dba778b 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -9,7 +9,7 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.SH OPTIONS
.TP
.B
-\fB\-n\fR name Specify the network name of the AirPlay server
+\fB\-n\fR name Specify the network name of the AirPlay server (UTF-8/ascii)
.TP
\fB\-nh\fR Do \fBNOT\fR append "@\fIhostname\fR" at end of AirPlay server name
.TP
diff --git a/uxplay.cpp b/uxplay.cpp
index 49ef86d..9e01127 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -91,6 +91,7 @@
#endif
static std::string server_name = DEFAULT_NAME;
+static bool server_name_is_utf8 = false;
static dnssd_t *dnssd = NULL;
static raop_t *raop = NULL;
static logger_t *render_logger = NULL;
@@ -839,7 +840,7 @@ static void print_info (char *name) {
printf("=========== Website: https://github.com/FDH2/UxPlay ==========\n");
printf("Usage: %s [-n name] [-s wxh] [-p [n]] [(other options)]\n", name);
printf("Options:\n");
- printf("-n name Specify the network name of the AirPlay server\n");
+ printf("-n name Specify network name of the AirPlay server (UTF-8/ascii)\n");
printf("-nh Do not add \"@hostname\" at the end of AirPlay server name\n");
printf("-h265 Support h265 (4K) video (with h265 versions of h264 plugins)\n");
printf("-hls [v] Support HTTP Live Streaming (currently Youtube video only) \n");
@@ -1067,8 +1068,62 @@ static void append_hostname(std::string &server_name) {
#endif
}
+bool is_utf8(const char *string, bool *is_printable_ascii) {
+ /* test if C-string is printable ascii or valid UTF-8 (max 4 bytes) */
+ if (is_printable_ascii) {
+ *is_printable_ascii = true;
+ }
+ int len = (int) strlen(string);
+ for (int i = 0; i < len; i++) {
+ unsigned char c = (unsigned char) string[i];
+ int n = 0;
+ if (0x20 <= c && c <= 0x7e) {
+ continue; //printable ascii, no control characters.
+ } else if (is_printable_ascii) {
+ *is_printable_ascii = false;
+ }
+ if (0x00 <= c && c <= 0x7f) {
+ continue; //one byte code, 0bbbbbbb
+ } else if (c == 0xc0 || c == 0xc1) {
+ return false; //two byte code, invalid start byte
+ } else if ((c & 0xe0) == 0xc0) {
+ n = 1; //two byte code, 110bbbbb
+ } else if (c == 0xe0 && i < len - 1 && (unsigned char) string[i + 1] < 0xa0) {
+ return false; //three byte code, overlong encoding
+ } else if (c == 0xed && i < len - 1 && (unsigned char) string[i + 1] > 0x9f) {
+ return false; //three byte code, exclude U+dc00 to U+dfff
+ } else if ((c & 0xf0) == 0xe0) {
+ n = 2; //three byte code 1110bbbb
+ } else if (c >= 0xf5) {
+ return false; //four byte code, invalid start byte
+ } else if (c == 0xf0 && i < len - 1 && (unsigned char) string[i + 1] < 0x90) {
+ return false; //four byte code, overlong encoding
+ } else if (c == 0xf4 && i < len - 1 && (unsigned char) string[i + 1] > 0x8f) {
+ return false; //four byte code, out of range character (> U+10ffff)
+ } else if ((c & 0xf8) == 0xf0) {
+ n = 3; //four byte code, 11110bbb
+ } else {
+ return false; //more than 4 bytes
+ }
+ for (int j = 0; j < n && i < len ; j++) { // n bytes matching 10bbbbbb must follow ?
+ if ((++i == len) || (((unsigned char) string[i] & 0xc0) != 0x80)) {
+ return false;
+ }
+ }
+
+ }
+ return true;
+}
+
static void parse_arguments (int argc, char *argv[]) {
// Parse arguments
+ for (int i = 1; i < argc; i++) {
+ if (!is_utf8(argv[i], NULL)) {
+ fprintf(stderr,"Error: detected a non-ascii or non-UTF-8 string \"%s\""
+ "while parsing input arguments", argv[i]);
+ exit(0);
+ }
+ }
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
if (arg == "-rc") {
@@ -1092,7 +1147,20 @@ static void parse_arguments (int argc, char *argv[]) {
restrict_clients = true;
} else if (arg == "-n") {
if (!option_has_value(i, argc, arg, argv[i+1])) exit(1);
- server_name = std::string(argv[++i]);
+ bool ascii;
+ server_name_is_utf8 = false;
+ server_name.erase();
+ bool utf8 = is_utf8(argv[++i], &ascii);
+ if (!utf8) {
+ fprintf(stderr, "invalid (non-UTF-8/ascii) server name in \"-n %s\"", argv[i]);
+ exit(1);
+ }
+ server_name = std::string(argv[i]);
+ if (!ascii) {
+ server_name_is_utf8 = true;
+ printf("WARNING: a non-ascii (UTF-8) server-name \"%s\" was specified:"
+ " ensure correct locale settings to display it\n",server_name.c_str());
+ }
} else if (arg == "-nh") {
do_append_hostname = false;
} else if (arg == "-async") {
From 24d986a541da382ed4eae0729fc54a5567ac835f Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Thu, 30 Oct 2025 16:42:39 -0400
Subject: [PATCH 49/58] update README to mention new Windows Terminal app and
UTF-8 support
---
README.html | 18 +++++++++++++++++-
README.md | 19 ++++++++++++++++++-
README.txt | 20 +++++++++++++++++++-
3 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/README.html b/README.html
index 9bd7620..a9f3437 100644
--- a/README.html
+++ b/README.html
@@ -1043,6 +1043,22 @@ is added.
The executable uxplay.exe can also be run without the MSYS2
environment, in the Windows Terminal, with
C:\msys64\ucrt64\bin\uxplay.
+There is a new modernized Windows Terminal application available from
+Microsoft that provides various terminals, and can be customized to also
+provide the MSYS2 terminals. See https://www.msys2.org/docs/terminals/
+(to make those instructions clearer: in the dropdown “Settings” menu,
+there is a second “settings” icon in the lower left corner: click on
+that to edit the settings.json file as described).
+The server name (-n option) can be given in internationalized
+UTF-8 encoding: To enter UTF-8 characters in the MSYS2 or other Windows
+terminals, use the numerical keypad with “Num Lock” on: while holding
+down the “Alt” key, type “+” on the keypad, followed by the UTF-8 hex
+code for the character (using the keypad for numbers), then release the
+“Alt” key. (The UTF-8 hex codes have 4 hex digits: for example, the
+“copyright” symbol has hex code 00a9.) This method must be activated in
+the Windows Registry: using regedit, find the Registry section
+’HKEY_Current_User/Control Panel/Input Method”, and add a new Key
+“EnableHexNumpad” with value “1”, then reboot the computer.
Usage
Options:
@@ -1862,7 +1878,7 @@ what version UxPlay claims to be.
screensaver while UxPlay is running (Linux/*BSD only). Add support for
Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
forwarding decrypted h264/5 video to an external renderer (e.g., OBS
-Studio)
+Studio). Check that option input strings have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced
with -pw option. Update llhttp to v 9.3.0
1.72.1 2025-06-06 minor update: fix regression in -reg option; add
diff --git a/README.md b/README.md
index a0ff8a6..b417992 100644
--- a/README.md
+++ b/README.md
@@ -1026,6 +1026,22 @@ that is apparently now fixed (a workaround is to use d3d11)._
The executable uxplay.exe can also be run without the MSYS2 environment,
in the Windows Terminal, with `C:\msys64\ucrt64\bin\uxplay`.
+There is a new modernized Windows Terminal application available from Microsoft that
+provides various terminals, and can be customized to also provide the MSYS2 terminals.
+See https://www.msys2.org/docs/terminals/ (to make those instructions clearer:
+in the dropdown "Settings" menu, there is a second "settings" icon in the lower left corner:
+click on that to edit the settings.json file as described).
+
+The server name (-n option) can be given in internationalized UTF-8 encoding:
+To enter UTF-8 characters in the MSYS2 or other Windows terminals, use the numerical keypad
+with "Num Lock" on: while holding down the "Alt" key, type "+" on the keypad, followed
+by the UTF-8 hex code for the character (using the keypad for numbers), then release the "Alt" key.
+(The UTF-8 hex codes have 4 hex digits: for example, the "copyright" symbol has hex code 00a9.)
+This method must be activated in the Windows Registry: using
+regedit, find the Registry section 'HKEY_Current_User/Control Panel/Input Method",
+and add a new Key "EnableHexNumpad" with value "1", then reboot the computer.
+
+
# Usage
Options:
@@ -1877,7 +1893,8 @@ xxxx 2025-09-25 Render Audio cover-art inside UxPlay with -ca option (no file
specified). (D-Bus based) option -scrsv to inhibit screensaver while UxPlay
is running (Linux/*BSD only). Add support for Service Discovery using a
Bluetooth LE beacon. Add -vrtp option for forwarding decrypted h264/5 video
-to an external renderer (e.g., OBS Studio)
+to an external renderer (e.g., OBS Studio). Check that option input strings
+have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with -pw
option. Update llhttp to v 9.3.0
diff --git a/README.txt b/README.txt
index fc3852b..ed15a37 100644
--- a/README.txt
+++ b/README.txt
@@ -1057,6 +1057,24 @@ d3d11).*
The executable uxplay.exe can also be run without the MSYS2 environment,
in the Windows Terminal, with `C:\msys64\ucrt64\bin\uxplay`.
+There is a new modernized Windows Terminal application available from
+Microsoft that provides various terminals, and can be customized to also
+provide the MSYS2 terminals. See https://www.msys2.org/docs/terminals/
+(to make those instructions clearer: in the dropdown "Settings" menu,
+there is a second "settings" icon in the lower left corner: click on
+that to edit the settings.json file as described).
+
+The server name (-n ``{=html} option) can be given in
+internationalized UTF-8 encoding: To enter UTF-8 characters in the MSYS2
+or other Windows terminals, use the numerical keypad with "Num Lock" on:
+while holding down the "Alt" key, type "+" on the keypad, followed by
+the UTF-8 hex code for the character (using the keypad for numbers),
+then release the "Alt" key. (The UTF-8 hex codes have 4 hex digits: for
+example, the "copyright" symbol has hex code 00a9.) This method must be
+activated in the Windows Registry: using regedit, find the Registry
+section 'HKEY_Current_User/Control Panel/Input Method", and add a new
+Key "EnableHexNumpad" with value "1", then reboot the computer.
+
# Usage
Options:
@@ -1926,7 +1944,7 @@ file specified). (D-Bus based) option -scrsv ``{=html} to inhibit
screensaver while UxPlay is running (Linux/\*BSD only). Add support for
Service Discovery using a Bluetooth LE beacon. Add -vrtp option for
forwarding decrypted h264/5 video to an external renderer (e.g., OBS
-Studio)
+Studio). Check that option input strings have valid UTF-8 encoding.
1.72.2 2025-07-07 Fix bug (typo) in DNS_SD advertisement introduced with
-pw option. Update llhttp to v 9.3.0
From 930316ee0cd17d5f6c327a2edfba0eb6f4fa15e7 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 31 Oct 2025 06:10:03 -0400
Subject: [PATCH 50/58] begin winrt beacon development
---
Bluetooth_LE_beacon/winrt/uxplay-beacon.py | 304 +++++++++++++++++++++
uxplay.cpp | 2 +-
2 files changed, 305 insertions(+), 1 deletion(-)
create mode 100755 Bluetooth_LE_beacon/winrt/uxplay-beacon.py
diff --git a/Bluetooth_LE_beacon/winrt/uxplay-beacon.py b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
new file mode 100755
index 0000000..f7a5671
--- /dev/null
+++ b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
@@ -0,0 +1,304 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# adapted from https://github.com/bluez/bluez/blob/master/test/example-advertisement
+#----------------------------------------------------------------
+# a standalone python-3.6 or later DBus-based AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
+# (c) F. Duncanh, October 2025
+
+import gi
+try:
+ from gi.repository import GLib
+except ImportError:
+ print(f"ImportError: failed to import GLib")
+
+
+
+def setup_beacon(ipv4_str, port, advmin, advmax, index):
+ print(f"setup_becaon port {ipv4_str}:{port} [{advin}:{advmax}] ({index})")
+
+def beacon_on():
+ print(f"beacon_on")
+ return True
+
+def beacon_off():
+ print(f"beacon off")
+
+#==generic code (non-dbus) below here =============
+
+import argparse
+import os
+import sys
+import psutil
+import struct
+import socket
+
+# global variables
+beacon_is_running = False
+beacon_is_pending_on = False
+beacon_is_pending_off = False
+
+port = int(0)
+advmin = int(100)
+advmax = int(100)
+ipv4_str = "ipv4_address"
+index = int(0)
+
+def start_beacon():
+ global beacon_is_running
+ global port
+ global ipv4_str
+ global advmin
+ global advmax
+ global index
+ setup_beacon(ipv4_str, port, advmin, advmax, index)
+ beacon_is_running = beacon_on()
+
+def stop_beacon():
+ global beacon_is_running
+ beacon_off()
+ beacon_is_running = False
+
+def check_process_name(pid, pname):
+ try:
+ process = psutil.Process(pid)
+ if process.name().find(pname,0) == 0:
+ return True
+ else:
+ return False
+ except psutil.NoSuchProcess:
+ return False
+
+def check_pending():
+ global beacon_is_running
+ global beacon_is_pending_on
+ global beacon_is_pending_off
+ if beacon_is_running:
+ if beacon_is_pending_off:
+ stop_beacon()
+ beacon_is_pending_off = False
+ else:
+ if beacon_is_pending_on:
+ start_beacon()
+ beacon_is_pending_on = False
+ return True
+
+
+def check_file_exists(file_path):
+ global port
+ global beacon_is_running
+ global beacon_is_pending_on
+ global beacon_is_pending_off
+
+ if os.path.exists(file_path):
+ with open(file_path, 'rb') as file:
+ data = file.read(2)
+ port = struct.unpack('= min):
+ raise ValueError('AdvMax was smaller than AdvMin')
+ if not (max <= 10240):
+ raise ValueError('AdvMax was larger than 10240 msecs')
+
+
+def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
+ global ipv4_str
+ global advmin
+ global advmax
+ global index
+ ipv4_str = ipv4_str_in
+ advmin = advmin_in
+ advmax = advmax_in
+ index = index_in
+
+ try:
+ while True:
+ try:
+ check_adv_intrvl(advmin, advmax)
+ except ValueError as e:
+ print(f'Error: {e}')
+ raise SystemExit(1)
+
+ GLib.timeout_add_seconds(5, on_timeout, file_path)
+ GLib.timeout_add_seconds(1, check_pending)
+ mainloop = GLib.MainLoop()
+ mainloop.run()
+ except KeyboardInterrupt:
+ print(f'\nExiting ...')
+ sys.exit(0)
+
+
+
+if __name__ == '__main__':
+
+
+ if not sys.version_info >= (3,6):
+ print("uxplay-beacon.py requires Python 3.6 or higher")
+
+ # Create an ArgumentParser object
+ parser = argparse.ArgumentParser(
+ description='A program that runs an AirPlay service discovery BLE beacon.',
+ epilog='Example: python beacon.py --ipv4 "192.168.1.100" --path "/home/user/ble" --AdvMin 100 --AdvMax 100"'
+ )
+
+ home_dir = os.path.expanduser("~")
+ # Add arguments
+ parser.add_argument(
+ '--file',
+ type=str,
+ default= home_dir + "/.uxplay.beacon",
+ help='beacon startup file (optional): one entry (key, value) per line, e.g. --ipv4 192.168.1.100, (lines startng with with # are ignored)'
+ )
+
+ parser.add_argument(
+ '--path',
+ type=str,
+ default= home_dir + "/.uxplay.ble",
+ help='path to AirPlay server BLE beacon information file (default: ~/.uxplay.ble)).'
+ )
+ parser.add_argument(
+ '--ipv4',
+ type=str,
+ default='use gethostbyname',
+ help='ipv4 address of AirPlay server (default: use gethostbyname).'
+ )
+
+ parser.add_argument(
+ '--AdvMin',
+ type=str,
+ default="0",
+ help='The minimum Advertising Interval (>= 100) units=msec, default 100)'
+ )
+ parser.add_argument(
+ '--AdvMax',
+ type=str,
+ default="0",
+ help='The maximum Advertising Interval (>= AdvMin, <= 10240) units=msec, default 100)'
+ )
+
+ parser.add_argument(
+ '--index',
+ type=str,
+ default="0",
+ help='use index >= 0 to distinguish multiple AirPlay Service Discovery beacons, default 0)'
+ )
+
+ # Parse the command-line argunts
+ args = parser.parse_args()
+ ipv4_str = None
+ path = None
+ advmin = int(100)
+ advmax = int(100)
+ index = int(0)
+
+ if args.file:
+ print(f'Using config file: {args.file}')
+ if os.path.exists(args.file):
+ with open(args.file, 'r') as file:
+ for line in file:
+ stripped_line = line.strip()
+ if stripped_line.startswith('#'):
+ continue
+ parts = stripped_line.partition(" ")
+ part0 = parts[0]
+ part2 = parts[2]
+ key = part0.strip()
+ value = part2.strip()
+ if key == "--path":
+ path = value
+ elif key == "--ipv4":
+ ipv4_str = value
+ elif key == "--AdvMin":
+ if value.isdigit():
+ advmin = int(value)
+ else:
+ print(f'Invalid config file input (--AdvMin) {value} in {args.file}')
+ raise SystemExit(1)
+ elif key == "--AdvMax":
+ if value.isdigit():
+ advmax = int(value)
+ else:
+ print(f'Invalid config file input (--AdvMax) {value} in {args.file}')
+ raise SystemExit(1)
+ elif key == "--index":
+ if value.isdigit():
+ index = int(value)
+ else:
+ print(f'Invalid config file input (--index) {value} in {args.file}')
+ raise SystemExit(1)
+ else:
+ print(f'Unknown key "{key}" in config file {args.file}')
+ raise SystemExit(1)
+
+ if args.ipv4 == "use gethostbyname":
+ if (ipv4_str is None):
+ ipv4_str = socket.gethostbyname(socket.gethostname())
+ else:
+ ipv4_str = args.ipv4
+
+ if args.AdvMin != "0":
+ if args.AdvMin.isdigit():
+ advmin = int(args.AdvMin)
+ else:
+ print(f'Invalid input (AdvMin) {args.AdvMin}')
+ raise SystemExit(1)
+
+ if args.AdvMax != "0":
+ if args.AdvMax.isdigit():
+ advmax = int(args.AdvMax)
+ else:
+ print(f'Invalid input (AdvMin) {args.AdvMin}')
+ raise SystemExit(1)
+
+ if args.index != "0":
+ if args.index.isdigit():
+ index = int(args.index)
+ else:
+ print(f'Invalid input (AdvMin) {args.AdvMin}')
+ raise SystemExit(1)
+ if index < 0:
+ raise ValueError('index was negative (forbidden)')
+
+ print(f'AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}, advmin:advmax {advmin}:{advmax} index:{index}')
+ print(f'(Press Ctrl+C to exit)')
+ main(args.path, ipv4_str, advmin, advmax, index)
+
diff --git a/uxplay.cpp b/uxplay.cpp
index 9e01127..9b6721c 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -2972,7 +2972,7 @@ int main (int argc, char *argv[]) {
#define PID_MAX 4194304 // 2^22
if (ble_filename.length()) {
-#ifdef _WIN_32
+#ifdef _WIN32
DWORD pid = GetCurrentProcessId();
g_assert(pid <= PID_MAX);
#else
From 7fab9107e612cad18cad5c79283fb27662b5b3ff Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Fri, 31 Oct 2025 23:53:46 -0400
Subject: [PATCH 51/58] winrt beacon cleanups
---
Bluetooth_LE_beacon/dbus/uxplay-beacon.py | 2 +-
Bluetooth_LE_beacon/winrt/uxplay-beacon.py | 18 +++++++-----------
uxplay.cpp | 6 +++---
3 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
index b73b073..939d303 100644
--- a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
@@ -260,7 +260,7 @@ def check_file_exists(file_path):
port = struct.unpack(' 15 ? 15 :len));
FILE *fp = fopen(filename, "wb");
printf("port %u\n", raop_port);
size_t count = sizeof(uint16_t) * fwrite(&raop_port, sizeof(uint16_t), 1, fp);
count += sizeof(uint32_t) * fwrite(pid, sizeof(uint32_t), 1, fp);
- count += fwrite(name, 1, sizeof(name), fp);
+ count += sizeof(char) * len * fwrite(process_name, 1, len * sizeof(char), fp);
fclose(fp);
return (int) count;
}
@@ -2973,7 +2972,8 @@ int main (int argc, char *argv[]) {
#define PID_MAX 4194304 // 2^22
if (ble_filename.length()) {
#ifdef _WIN32
- DWORD pid = GetCurrentProcessId();
+ DWORD winpid = GetCurrentProcessId();
+ uint32_t pid = (uint32_t) winpid;
g_assert(pid <= PID_MAX);
#else
pid_t pid = getpid();
From f6b2c0772d11ecb07c5870a7f2c2195bcac4f31e Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 1 Nov 2025 17:22:55 -0400
Subject: [PATCH 52/58] windows beacon seems to be working
---
Bluetooth_LE_beacon/winrt/uxplay-beacon.py | 81 ++++++++++++++++++++--
1 file changed, 77 insertions(+), 4 deletions(-)
diff --git a/Bluetooth_LE_beacon/winrt/uxplay-beacon.py b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
index 313ed0f..3c35570 100755
--- a/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
@@ -4,25 +4,98 @@
# a standalone python-3.6 or later winrt-based AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
# (c) F. Duncanh, October 2025
+
import gi
try:
from gi.repository import GLib
except ImportError:
print(f"ImportError: failed to import GLib")
-
+# Import WinRT APIs
+
+try:
+ import winrt.windows.foundation.collections
+except ImportError:
+ print(f"ImportError from winrt-Windows.Foundation.Collections")
+ print(f"Install with 'pip install winrt-Windows.Foundation.Collections'")
+
+try:
+ import winrt.windows.devices.bluetooth.advertisement as ble_adv
+except ImportError:
+ print(f"ImportError from winrt-Windows.Devices.Bluetooth.Advertisement")
+ print(f"Install with 'pip install winrt-Windows.Devices.Bluetooth.Advertisement'")
+
+try:
+ import winrt.windows.storage.streams as streams
+except ImportError:
+ print(f"ImportError from winrt-Windows.Storage.Streams")
+ print(f"Install with 'pip install winrt-Windows.Storage.Streams'")
+
+import struct
+import ipaddress
+import asyncio
+
+#global variables used by winrt.windows.devices.bluetooth.advertisement code
+publisher = None
+
+def on_status_changed(sender, args):
+ print(f"Publisher status: {args.status}")
+
+
+def create_airplay_service_discovery_advertisement_publisher(ipv4_str, port):
+ assert port > 0
+ assert port <= 65535
+ mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30
+ ipv4_address = ipaddress.ip_address(ipv4_str)
+ ipv4 = bytearray(ipv4_address.packed)
+ mfg_data.extend(ipv4)
+ port_bytes = port.to_bytes(2, 'big')
+ mfg_data.extend(port_bytes)
+ writer = streams.DataWriter()
+ writer.write_bytes(mfg_data)
+ manufacturer_data = ble_adv.BluetoothLEManufacturerData()
+ manufacturer_data.company_id = 0x004C #Apple
+ manufacturer_data.data = writer.detach_buffer()
+ advertisement = ble_adv.BluetoothLEAdvertisement()
+ advertisement.manufacturer_data.append(manufacturer_data)
+ global publisher
+ publisher = ble_adv.BluetoothLEAdvertisementPublisher(advertisement)
+ publisher.add_status_changed(on_status_changed)
+
+async def publish_advertisement():
+ try:
+ publisher.start()
+ print(f"Publisher started successfully")
+
+ except Exception as e:
+ print(f"Failed to start Publihser: {e}")
+ ptint(f"Publisher Status: {publisher.status}")
+
+
def setup_beacon(ipv4_str, port, advmin, advmax, index):
- print(f"setup_beacon port {ipv4_str}:{port} [{advmin}:{advmax}] ({index})")
+ #index will be ignored
+ print(f"setup_beacon port {ipv4_str}:{port} Note: min, max advertising interval and index are not used)")
+ create_airplay_service_discovery_advertisement_publisher(ipv4_str, port)
def beacon_on():
print(f"beacon_on")
- return True
+ global publisher
+ try:
+ asyncio.run( publish_advertisement())
+ return True
+ except Exception as e:
+ print(f"Failed to start publisher: {e}")
+ return False
+
def beacon_off():
print(f"beacon off")
+ global publisher
+ publisher.stop()
+ publisher = None
-#==generic code (non-dbus) below here =============
+#==generic code (non-winrt) below here =============
import argparse
import os
From 671fd9a7154efa4398478b7d2a5673ee71cad32d Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sat, 1 Nov 2025 17:24:48 -0400
Subject: [PATCH 53/58] dbus becaon cleanup
---
Bluetooth_LE_beacon/dbus/uxplay-beacon.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
index 939d303..603cad2 100644
--- a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
@@ -114,8 +114,8 @@ class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement):
assert port <= 65535
mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30
import ipaddress
- ipv4_address = ipaddress.ip_address(ipv4_str);
- ipv4 = bytearray(ipv4_address.packed);
+ ipv4_address = ipaddress.ip_address(ipv4_str)
+ ipv4 = bytearray(ipv4_address.packed)
mfg_data.extend(ipv4)
port_bytes = port.to_bytes(2, 'big')
mfg_data.extend(port_bytes)
From d92836e387a958ba942678e20492083c22852334 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 2 Nov 2025 01:07:28 -0400
Subject: [PATCH 54/58] cleanups, working winrt beacon
---
Bluetooth_LE_beacon/winrt/uxplay-beacon.py | 135 +++------------------
1 file changed, 14 insertions(+), 121 deletions(-)
diff --git a/Bluetooth_LE_beacon/winrt/uxplay-beacon.py b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
index 3c35570..8821511 100755
--- a/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
@@ -4,14 +4,12 @@
# a standalone python-3.6 or later winrt-based AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
# (c) F. Duncanh, October 2025
-
import gi
try:
from gi.repository import GLib
except ImportError:
print(f"ImportError: failed to import GLib")
-
# Import WinRT APIs
try:
@@ -40,7 +38,7 @@ import asyncio
publisher = None
def on_status_changed(sender, args):
- print(f"Publisher status: {args.status}")
+ print(f"Publisher status change to: {args.status.name}")
def create_airplay_service_discovery_advertisement_publisher(ipv4_str, port):
@@ -69,17 +67,16 @@ async def publish_advertisement():
print(f"Publisher started successfully")
except Exception as e:
- print(f"Failed to start Publihser: {e}")
- ptint(f"Publisher Status: {publisher.status}")
+ print(f"Failed to start Publisher: {e}")
+ print(f"Publisher Status: {publisher.status.name}")
-def setup_beacon(ipv4_str, port, advmin, advmax, index):
+def setup_beacon(ipv4_str, port):
#index will be ignored
- print(f"setup_beacon port {ipv4_str}:{port} Note: min, max advertising interval and index are not used)")
+ print(f"setup_beacon port {ipv4_str}:{port}")
create_airplay_service_discovery_advertisement_publisher(ipv4_str, port)
def beacon_on():
- print(f"beacon_on")
global publisher
try:
asyncio.run( publish_advertisement())
@@ -90,9 +87,9 @@ def beacon_on():
def beacon_off():
- print(f"beacon off")
global publisher
publisher.stop()
+ print(f"Current Publisher Status: {publisher.status.name}")
publisher = None
#==generic code (non-winrt) below here =============
@@ -110,19 +107,13 @@ beacon_is_pending_on = False
beacon_is_pending_off = False
port = int(0)
-advmin = int(100)
-advmax = int(100)
ipv4_str = "ipv4_address"
-index = int(0)
def start_beacon():
global beacon_is_running
global port
global ipv4_str
- global advmin
- global advmax
- global index
- setup_beacon(ipv4_str, port, advmin, advmax, index)
+ setup_beacon(ipv4_str, port)
beacon_is_running = beacon_on()
def stop_beacon():
@@ -188,45 +179,13 @@ def on_timeout(file_path):
return True
-def process_input(value):
- try:
- my_integer = int(value)
- return my_integer
- except ValueError:
- print(f'Error: could not convert "{value}" to integer: {my_integer}')
- return None
-
-
-
-#check AdvInterval
-def check_adv_intrvl(min, max):
- if not (100 <= min):
- raise ValueError('AdvMin was smaller than 100 msecs')
- if not (max >= min):
- raise ValueError('AdvMax was smaller than AdvMin')
- if not (max <= 10240):
- raise ValueError('AdvMax was larger than 10240 msecs')
-
-
-def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
+def main(file_path, ipv4_str_in):
global ipv4_str
- global advmin
- global advmax
- global index
ipv4_str = ipv4_str_in
- advmin = advmin_in
- advmax = advmax_in
- index = index_in
-
+
try:
while True:
- try:
- check_adv_intrvl(advmin, advmax)
- except ValueError as e:
- print(f'Error: {e}')
- raise SystemExit(1)
-
- GLib.timeout_add_seconds(5, on_timeout, file_path)
+ GLib.timeout_add_seconds(2, on_timeout, file_path)
GLib.timeout_add_seconds(1, check_pending)
mainloop = GLib.MainLoop()
mainloop.run()
@@ -237,15 +196,13 @@ def main(file_path, ipv4_str_in, advmin_in, advmax_in, index_in):
if __name__ == '__main__':
-
-
if not sys.version_info >= (3,6):
print("uxplay-beacon.py requires Python 3.6 or higher")
# Create an ArgumentParser object
parser = argparse.ArgumentParser(
- description='A program that runs an AirPlay service discovery BLE beacon.',
- epilog='Example: python beacon.py --ipv4 "192.168.1.100" --path "/home/user/ble" --AdvMin 100 --AdvMax 100"'
+ description='A program (for MS Windows systems only) that runs an AirPlay service discovery BLE beacon.',
+ epilog='Example: python beacon.py --ipv4 "192.168.1.100" --path "/home/user/ble"'
)
home_dir = os.environ.get("HOME")
@@ -271,33 +228,10 @@ if __name__ == '__main__':
help='ipv4 address of AirPlay server (default: use gethostbyname).'
)
- parser.add_argument(
- '--AdvMin',
- type=str,
- default="0",
- help='The minimum Advertising Interval (>= 100) units=msec, default 100)'
- )
- parser.add_argument(
- '--AdvMax',
- type=str,
- default="0",
- help='The maximum Advertising Interval (>= AdvMin, <= 10240) units=msec, default 100)'
- )
-
- parser.add_argument(
- '--index',
- type=str,
- default="0",
- help='use index >= 0 to distinguish multiple AirPlay Service Discovery beacons, default 0)'
- )
-
# Parse the command-line argunts
args = parser.parse_args()
ipv4_str = None
path = None
- advmin = int(100)
- advmax = int(100)
- index = int(0)
if args.file:
print(f'Using config file: {args.file}')
@@ -316,24 +250,6 @@ if __name__ == '__main__':
path = value
elif key == "--ipv4":
ipv4_str = value
- elif key == "--AdvMin":
- if value.isdigit():
- advmin = int(value)
- else:
- print(f'Invalid config file input (--AdvMin) {value} in {args.file}')
- raise SystemExit(1)
- elif key == "--AdvMax":
- if value.isdigit():
- advmax = int(value)
- else:
- print(f'Invalid config file input (--AdvMax) {value} in {args.file}')
- raise SystemExit(1)
- elif key == "--index":
- if value.isdigit():
- index = int(value)
- else:
- print(f'Invalid config file input (--index) {value} in {args.file}')
- raise SystemExit(1)
else:
print(f'Unknown key "{key}" in config file {args.file}')
raise SystemExit(1)
@@ -344,30 +260,7 @@ if __name__ == '__main__':
else:
ipv4_str = args.ipv4
- if args.AdvMin != "0":
- if args.AdvMin.isdigit():
- advmin = int(args.AdvMin)
- else:
- print(f'Invalid input (AdvMin) {args.AdvMin}')
- raise SystemExit(1)
-
- if args.AdvMax != "0":
- if args.AdvMax.isdigit():
- advmax = int(args.AdvMax)
- else:
- print(f'Invalid input (AdvMin) {args.AdvMin}')
- raise SystemExit(1)
-
- if args.index != "0":
- if args.index.isdigit():
- index = int(args.index)
- else:
- print(f'Invalid input (AdvMin) {args.AdvMin}')
- raise SystemExit(1)
- if index < 0:
- raise ValueError('index was negative (forbidden)')
-
- print(f'AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}, advmin:advmax {advmin}:{advmax} index:{index}')
+ print(f'AirPlay Service-Discovery Bluetooth LE beacon: using BLE file {args.path}')
print(f'(Press Ctrl+C to exit)')
- main(args.path, ipv4_str, advmin, advmax, index)
+ main(args.path, ipv4_str)
From 4910930c56624f476e806eaf36d36926750b4b1d Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 2 Nov 2025 01:10:25 -0500
Subject: [PATCH 55/58] reworked signal handling (ctrl-c) for linux and windows
---
uxplay.cpp | 105 +++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 78 insertions(+), 27 deletions(-)
diff --git a/uxplay.cpp b/uxplay.cpp
index 5195957..c3d7ef3 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -22,7 +22,6 @@
#include
#include
-#include
#include
#include
#include
@@ -42,7 +41,9 @@
#include
#include
#include
+#include //for pthreads in MSYS2 UCRT
#else
+#include
#include
#include
#include
@@ -192,7 +193,7 @@ static std::string artist;
static std::string coverart_artist;
static std::string ble_filename = "";
static std::string rtp_pipeline = "";
-
+static GMainLoop *gmainloop = NULL;
//Support for D-Bus-based screensaver inhibition (org.freedesktop.ScreenSaver)
static unsigned int scrsv;
@@ -550,6 +551,39 @@ static gboolean x11_window_callback(gpointer loop) {
return FALSE;
}
+/* signals handlers (ctrl-c, etc )*/
+
+static void cleanup();
+
+#ifdef _WIN32
+static gboolean handle_signal(gpointer data) {
+ relaunch_video = false;
+ g_main_loop_quit(gmainloop);
+ return G_SOURCE_REMOVE;
+}
+
+static BOOL WINAPI CtrlHandler(DWORD signal) {
+ switch (signal) {
+ case CTRL_C_EVENT:
+ case CTRL_CLOSE_EVENT:
+ case CTRL_SHUTDOWN_EVENT:
+ if (gmainloop) {
+ g_idle_add(handle_signal, NULL);
+ return TRUE;
+ } else {
+ cleanup();
+ exit(0);
+ }
+ default:
+ return FALSE;
+ }
+}
+#else
+static void CtrlHandler(int signum) {
+ cleanup();
+ exit(0);
+}
+
static gboolean sigint_callback(gpointer loop) {
relaunch_video = false;
g_main_loop_quit((GMainLoop *) loop);
@@ -562,24 +596,10 @@ static gboolean sigterm_callback(gpointer loop) {
return TRUE;
}
-#ifdef _WIN32
-struct signal_handler {
- GSourceFunc handler;
- gpointer user_data;
-};
-
-static std::unordered_map u = {};
-
-static void SignalHandler(int signum) {
- if (signum == SIGTERM || signum == SIGINT) {
- u[signum].handler(u[signum].user_data);
- }
-}
-
-static guint g_unix_signal_add(gint signum, GSourceFunc handler, gpointer user_data) {
- u[signum] = signal_handler{handler, user_data};
- (void) signal(signum, SignalHandler);
- return 0;
+static gboolean sighup_callback(gpointer loop) {
+ relaunch_video = false;
+ g_main_loop_quit((GMainLoop *) loop);
+ return TRUE;
}
#endif
@@ -665,10 +685,30 @@ static void main_loop() {
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);
+
+#ifdef _WIN32
+ gmainloop = loop;
+#else
+ signal(SIGINT, SIG_DFL);
+ signal(SIGTERM, SIG_DFL);
+ signal(SIGHUP, SIG_DFL);
guint sigterm_watch_id = g_unix_signal_add(SIGTERM, (GSourceFunc) sigterm_callback, (gpointer) loop);
guint sigint_watch_id = g_unix_signal_add(SIGINT, (GSourceFunc) sigint_callback, (gpointer) loop);
+ guint sighup_watch_id = g_unix_signal_add(SIGHUP, (GSourceFunc) sigint_callback, (gpointer) loop);
+#endif
g_main_loop_run(loop);
+#ifdef _WIN32
+ gmainloop = NULL;
+#else
+ signal(SIGINT, CtrlHandler); //switch back to non-mainloop CtrlHandler
+ signal(SIGTERM, CtrlHandler);
+ signal(SIGHUP, CtrlHandler);
+ if (sigint_watch_id > 0) g_source_remove(sigint_watch_id);
+ if (sigterm_watch_id > 0) g_source_remove(sigterm_watch_id);
+ if (sighup_watch_id > 0) g_source_remove(sighup_watch_id);
+#endif
+
for (int i = 0; i < n_video_renderers; i++) {
if (gst_video_bus_watch_id[i] > 0) g_source_remove(gst_video_bus_watch_id[i]);
}
@@ -676,8 +716,6 @@ static void main_loop() {
if (gst_audio_bus_watch_id[i] > 0) g_source_remove(gst_audio_bus_watch_id[i]);
}
if (gst_x11_window_id > 0) g_source_remove(gst_x11_window_id);
- if (sigint_watch_id > 0) g_source_remove(sigint_watch_id);
- if (sigterm_watch_id > 0) g_source_remove(sigterm_watch_id);
if (reset_watch_id > 0) g_source_remove(reset_watch_id);
if (progress_id > 0) g_source_remove(progress_id);
if (video_reset_watch_id > 0) g_source_remove(video_reset_watch_id);
@@ -2669,6 +2707,17 @@ int main (int argc, char *argv[]) {
std::vector server_hw_addr;
std::string config_file = "";
+#ifdef _WIN32
+ if (!SetConsoleCtrlHandler(CtrlHandler, TRUE)) {
+ LOGE("Could not set control handler");
+ exit(1);
+ }
+#else
+ signal(SIGINT, CtrlHandler);
+ signal(SIGTERM, CtrlHandler);
+ signal(SIGHUP, CtrlHandler);
+#endif
+
#ifdef __OpenBSD__
if (unveil("/", "rwc") == -1 || unveil(NULL, NULL) == -1) {
err(1, "unveil");
@@ -2962,11 +3011,11 @@ int main (int argc, char *argv[]) {
}
if (start_dnssd(server_hw_addr, server_name)) {
- goto cleanup;
+ cleanup();
}
if (start_raop_server(display, tcp, udp, debug_log)) {
stop_dnssd();
- goto cleanup;
+ cleanup();
}
#define PID_MAX 4194304 // 2^22
@@ -2986,12 +3035,11 @@ int main (int argc, char *argv[]) {
if (register_dnssd()) {
stop_raop_server();
stop_dnssd();
- goto cleanup;
+ cleanup();
}
reconnect:
compression_type = 0;
close_window = new_window_closing_behavior;
-
main_loop();
if (relaunch_video) {
if (reset_httpd) {
@@ -3025,7 +3073,10 @@ int main (int argc, char *argv[]) {
stop_raop_server();
stop_dnssd();
}
- cleanup:
+ cleanup();
+}
+
+static void cleanup() {
if (use_audio) {
audio_renderer_destroy();
}
From 5df5e56ac271d4342852acb29f3fc7fb32211517 Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Sun, 2 Nov 2025 17:53:41 -0500
Subject: [PATCH 56/58] add uxplay-beacon.py for windows
---
.../dbus/uxplay-beacon.1 | 2 +-
Bluetooth_LE_beacon/dbus/uxplay-beacon.py | 26 ++++++++-----
Bluetooth_LE_beacon/winrt/uxplay-beacon.1 | 39 +++++++++++++++++++
Bluetooth_LE_beacon/winrt/uxplay-beacon.py | 28 +++++++------
CMakeLists.txt | 12 +++++-
5 files changed, 83 insertions(+), 24 deletions(-)
rename uxplay-beacon.1 => Bluetooth_LE_beacon/dbus/uxplay-beacon.1 (93%)
create mode 100644 Bluetooth_LE_beacon/winrt/uxplay-beacon.1
diff --git a/uxplay-beacon.1 b/Bluetooth_LE_beacon/dbus/uxplay-beacon.1
similarity index 93%
rename from uxplay-beacon.1
rename to Bluetooth_LE_beacon/dbus/uxplay-beacon.1
index edfa117..2655abf 100644
--- a/uxplay-beacon.1
+++ b/Bluetooth_LE_beacon/dbus/uxplay-beacon.1
@@ -5,7 +5,7 @@ uxplay-beacon.py \- Python (>= 3.6) script for a Bluetooth LE Service-Discovery
.B uxplay-beacon.py
[\fI\, -h, --help] + more options.
.SH DESCRIPTION
-UxPlay 1.72: Standalone Python Script for managing Bluetooth LE Service Discovery.
+UxPlay 1.72: Standalone Python Script for Bluetooth LE Service Discovery (DBus).
.SH OPTIONS
.TP
.B
diff --git a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
index 603cad2..9bb4deb 100644
--- a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
@@ -8,8 +8,8 @@
import gi
try:
from gi.repository import GLib
-except ImportError:
- print(f"ImportError: failed to import GLib")
+except ImportError as e:
+ print(f"ImportError: {e}, failed to import GLib")
import dbus
@@ -196,6 +196,7 @@ import sys
import psutil
import struct
import socket
+import time
# global variables
beacon_is_running = False
@@ -222,7 +223,10 @@ def stop_beacon():
global beacon_is_running
beacon_off()
beacon_is_running = False
-
+
+def pid_is_running(pid):
+ return psutil.pid_exists(pid)
+
def check_process_name(pid, pname):
try:
process = psutil.Process(pid)
@@ -260,10 +264,13 @@ def check_file_exists(file_path):
port = struct.unpack('= 3.6) script for a Bluetooth LE Service-Discovery beacon.
+.SH SYNOPSIS
+.B uxplay-beacon.py
+[\fI\, -h, --help] + more options.
+.SH DESCRIPTION
+UxPlay 1.72: Standalone Python Script for Bluetooth LE Service Discovery (Windows).
+.SH OPTIONS
+.TP
+.B
+\fB\--file\fR fn Specify alternate configuration file
+.TP
+\fB\--path\fR fn Specify non-default Bluetooth LE data file used by uxplay
+.TP
+\fB\--ipv4\fR ip Override automatically-obtained ipv4 address for contacting UxPlay
+.TP
+\fB \-h, --help\fR Show help text.
+.SH
+FILES
+Options in beacon configuration file ~/.uxplay.beacon
+.TP
+are applied first (command-line options may modify them). Format:
+.TP
+one option per line, with initial "--"; lines beginning with "#" ignored.
+.SH
+AUTHORS
+.TP
+Various, see website or distribution.
+.SH
+COPYRIGHT
+.TP
+Various, see website or distribution. License: GPL v3+:
+.TP
+GNU GPL version 3 or later. (some parts LGPL v.2.1+ or MIT).
+.SH
+SEE ALSO
+.TP
+Website:
diff --git a/Bluetooth_LE_beacon/winrt/uxplay-beacon.py b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
index 8821511..464c95d 100755
--- a/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/winrt/uxplay-beacon.py
@@ -38,8 +38,10 @@ import asyncio
publisher = None
def on_status_changed(sender, args):
+ global publisher
print(f"Publisher status change to: {args.status.name}")
-
+ if args.status.name == "STOPPED":
+ publisher = None
def create_airplay_service_discovery_advertisement_publisher(ipv4_str, port):
assert port > 0
@@ -73,7 +75,7 @@ async def publish_advertisement():
def setup_beacon(ipv4_str, port):
#index will be ignored
- print(f"setup_beacon port {ipv4_str}:{port}")
+ print(f"setup_beacon for {ipv4_str}:{port}")
create_airplay_service_discovery_advertisement_publisher(ipv4_str, port)
def beacon_on():
@@ -89,8 +91,6 @@ def beacon_on():
def beacon_off():
global publisher
publisher.stop()
- print(f"Current Publisher Status: {publisher.status.name}")
- publisher = None
#==generic code (non-winrt) below here =============
@@ -100,6 +100,7 @@ import sys
import psutil
import struct
import socket
+import time
# global variables
beacon_is_running = False
@@ -121,6 +122,9 @@ def stop_beacon():
beacon_off()
beacon_is_running = False
+def pid_is_running(pid):
+ return psutil.pid_exists(pid)
+
def check_process_name(pid, pname):
try:
process = psutil.Process(pid)
@@ -158,10 +162,13 @@ def check_file_exists(file_path):
port = struct.unpack('= (3,6):
print("uxplay-beacon.py requires Python 3.6 or higher")
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b1f8b63..4e50496 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -78,10 +78,18 @@ install( FILES uxplay.service DESTINATION ${CMAKE_INSTALL_DOCDIR}/systemd )
if (DBUS_FOUND)
install( FILES Bluetooth_LE_beacon/dbus/uxplay-beacon.py
DESTINATION bin
- PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ)
-install( FILES uxplay-beacon.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
+ PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ)
+install( FILES Bluetooth_LE_beacon/dbus/uxplay-beacon.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
endif()
+if (WIN32)
+install( FILES Bluetooth_LE_beacon/winrt/uxplay-beacon.py
+ DESTINATION bin
+ PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ)
+install( FILES Bluetooth_LE_beacon/winrt/uxplay-beacon.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 )
+endif()
+
+
# uninstall target
if(NOT TARGET uninstall)
From 54893050315767726d5fd5d6a5fa2640aab24e4f Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Mon, 3 Nov 2025 01:50:23 -0500
Subject: [PATCH 57/58] update README for Bluetooth LE service discovery
---
README.html | 75 ++++++++++++++++++++++++++++-------------------
README.md | 39 ++++++++++++++++---------
README.txt | 84 +++++++++++++++++++++++++++++++----------------------
3 files changed, 119 insertions(+), 79 deletions(-)
diff --git a/README.html b/README.html
index a9f3437..c1cc63d 100644
--- a/README.html
+++ b/README.html
@@ -9,18 +9,19 @@ class="uri">https://github.com/FDH2/UxPlay (where ALL user issues
should be posted, and latest versions can be found).
NEW on github: Support for service
-discovery using a Bluetooth LE “beacon” (as an alternative to
-Bonjour/Rendezvous DNS-SD service discovery). The user must set up a
-Bluetooth LE “beacon”, (a USB 4.0 or later “dongle” can be used). See
-instructions below. The beacon runs independently of UxPlay and
-regularly broadcasts a Bluetooth LE (“Low Energy”) 46 byte packet
-informing nearby iOS/macOS devices of the local IPv4 network address of
-the UxPlay server, and which TCP port to contact UxPlay on. A python
-script (Python >=3.6) “uxplay-beacon.py”, to broadcast the
-Service-Discovery advertisement will be installed on systems with DBus
-support (Linux and *BSD, using BlueZ for Bluetooth control): this does
-not require enhanced “root permissions” to run. A
-windows version of this script is also planned for the future.
+discovery using a Bluetooth LE “beacon” for both Linux/*BSD and
+Windows (as an alternative to Bonjour/Rendezvous DNS-SD service
+discovery). The user must set up a Bluetooth LE “beacon”, (a USB 4.0 or
+later “dongle” can be used). See instructions below. The beacon runs
+independently of UxPlay and regularly broadcasts a Bluetooth LE (“Low
+Energy”) 46 byte packet informing nearby iOS/macOS devices of the local
+IPv4 network address of the UxPlay server, and which TCP port to contact
+UxPlay on. Two versions of a Python script (Python >=3.6)
+“uxplay-beacon.py”, (one for Linux/*BSD using BlueZ
+LEAdvertisingManager1 with DBus, and one for Windows using
+winrt/BluetoothLEAdvertisementPublisher) are ready for users to run: the
+appropriate version will be installed when UxPlay is built. They
+independently run Service-Discovery beacons that iOS devices respond to.
Instructions are given
below.
NEW on github: option
@@ -1472,17 +1473,24 @@ messages, set GST_DEBUG=4; for DEBUG messages, GST_DEBUG=5; increase
this to see even more of the GStreamer inner workings.
Bluetooth LE beacon setup
The python>=3.6 script for running a Bluetooth-LE Service
-Discovery beacon is uxplay-beacon.py. Currently only a DBus version (for
-Linux and *BSD) is available, and it is only installed on systems which
-support DBus. Bluetooth >= 4.0 hardware on the host computer is
-required: a cheap USB bluetooth dongle can be used. Bluetooth support
-(BlueZ) must be installed (on Debian-based systems:
-sudo apt install bluez bluez-tools; recent Ubuntu releases
-provide bluez as a snap package).
-In addition to standard Python3 libraries, you may need to install
-the gi, dbus, and psutil Python libraries used by uxplay-beacon.py. On
-Debian-based systems:
+Discovery beacon is uxplay-beacon.py. It comes in two versions, one (for
+Linux and *BSD) is only installed on systems which support DBUS, and
+another only for Windows 10/11. Bluetooth >= 4.0 hardware on the host
+computer is required: a cheap USB bluetooth dongle can be used.
+On Linux/*BSD, Bluetooth support (BlueZ) must be installed (on
+Debian-based systems: sudo apt install bluez bluez-tools;
+recent Ubuntu releases provide bluez as a snap package). In addition to
+standard Python3 libraries, you may need to install the gi, dbus, and
+psutil Python libraries used by uxplay-beacon.py. On Debian-based
+systems:
sudo apt install python3-gi python3-dbus python3-psutil
+For Windows support on MSYS2 UCRT systems, use pacman -S to install
+mingw-w64-ucrt-x86_64-python,
+*-python-gobject, *-python-psutil, and
+*-python-pip. Then install winrt bindings
+pip install winrt-Windows.Foundation.Collections,
+winrt-Windows.Devices.Bluetooth.Advertisement and
+winrt-Windows.Storage.Streams.
If uxplay will be run with option “uxplay -ble” (so it
writes data for the Bluetooth beacon in the default BLE data file
~/.uxplay.ble), just run uxplay-beacon.py in a
@@ -1492,13 +1500,14 @@ checking if the BLE data file exists, and stop when it no longer detects
a running UxPlay plus this file (it will restart advertising if UxPlay
later reappears). The script will remain active until stopped with
Ctrl+C in its terminal window (or its terminal window is closed).
-The beacon script can be more finely controlled using five possible
+
The beacon script can be more finely controlled using certain
options: these can be given on the command line, or read from a
configuration file ~/.uxplay.beacon, if it exists.
Configuration file entries are like the command line forms, one per line
(e.g., --ipv4 192.168.1.100). Lines commented out with an
initial # are ignored. Command line options override the
-configuration file options.
+configuration file options. Get help with man uxplay-beacon
+or uxplay-beacon.py --help. Options are
+The BlueZ/Dbus version has thee more options not offered by the
+Windows version:
+
--AdvMin x, --AdvMax y. These controls
the interval between BLE advertisement broadcasts. This interval is in
the range [x, y], given in units of msecs. Allowed ranges are 100 <=
@@ -1534,15 +1547,17 @@ can disable DNS_SD Service discovery by the avahi-daemon with
$ sudo systemctl stop avahi-daemon
To restore DNS_SD Service discovery, replace “mask” by “unmask”, and
“stop” by “start”.
+On Windows, the Bonjour Service is controlled using Services
+Management: press “Windows + R” to open the Run dialog, run
+services.msc, and click on Bonjour Service
+in the alphabetic list. This will show links for it to be stopped and
+restarted.
For more information, see the wiki
-page This has useful information if you wish to build a python
-beacon controller script for Windows (we would like to have one!).
+page
-- Our current understanding is that Bluetooth LE AirPlay
-Service Discovery only supports broadcast of IPv4 addresses. Please let
-us know if this is incorrect, or if IPv6 support is introduced in the
-future.
+- Note that Bluetooth LE AirPlay Service Discovery only
+supports broadcast of IPv4 addresses.
Troubleshooting
Note: uxplay is run from a terminal command line, and
diff --git a/README.md b/README.md
index b417992..b6eff8e 100644
--- a/README.md
+++ b/README.md
@@ -2,13 +2,12 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
-- **NEW on github**: Support for **service discovery using a Bluetooth LE "beacon"** (as an alternative to Bonjour/Rendezvous DNS-SD
+- **NEW on github**: Support for **service discovery using a Bluetooth LE "beacon"** for both Linux/\*BSD and Windows (as an alternative to Bonjour/Rendezvous DNS-SD
service discovery). The user must set up a Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used). See instructions
below. The beacon runs independently of UxPlay and regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet informing nearby iOS/macOS devices of
- the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on. A python script (Python >=3.6) "uxplay-beacon.py",
- to broadcast the Service-Discovery advertisement will be installed on systems with DBus support (Linux and *BSD, using BlueZ for Bluetooth control):
- this does **not** require enhanced "root permissions" to run.
- A windows version of this script is also planned for the future.
+ the local IPv4 network address of the UxPlay server, and which TCP port to contact UxPlay on. Two versions of a Python script (Python >=3.6) "uxplay-beacon.py",
+ (one for Linux/*BSD using BlueZ LEAdvertisingManager1 with DBus, and one for Windows using winrt/BluetoothLEAdvertisementPublisher) are ready for users to
+ run: the appropriate version will be installed when UxPlay is built. They independently run Service-Discovery beacons that iOS devices respond to.
Instructions are [given below](#bluetooth-le-beacon-setup).
- **NEW on github**: option `-vrtp ` bypasses rendering by UxPlay, and instead
@@ -1491,11 +1490,13 @@ GStreamer inner workings.
# Bluetooth LE beacon setup
The python>=3.6 script for running a Bluetooth-LE Service Discovery beacon is uxplay-beacon.py.
-Currently only a DBus version (for Linux and *BSD) is available, and it is only installed on systems which
-support DBus. Bluetooth >= 4.0 hardware on the host computer is required: a cheap USB bluetooth dongle
-can be used. Bluetooth support (BlueZ) must be installed (on Debian-based systems: `sudo apt install bluez bluez-tools`;
-recent Ubuntu releases provide bluez as a snap package).
+It comes in two versions, one (for Linux and *BSD) is only installed on systems which
+support DBUS, and another only for Windows 10/11. Bluetooth >= 4.0 hardware on the host computer is required: a cheap USB bluetooth dongle
+can be used.
+On Linux/*BSD,
+Bluetooth support (BlueZ) must be installed (on Debian-based systems: `sudo apt install bluez bluez-tools`;
+recent Ubuntu releases provide bluez as a snap package).
In addition to standard Python3 libraries, you may need to install the gi, dbus, and psutil Python libraries used by
uxplay-beacon.py. On Debian-based systems:
@@ -1503,6 +1504,12 @@ uxplay-beacon.py. On Debian-based systems:
sudo apt install python3-gi python3-dbus python3-psutil
```
+For Windows support on MSYS2 UCRT systems, use pacman -S to
+install `mingw-w64-ucrt-x86_64-python`, ``*-python-gobject``,
+`*-python-psutil`, and ``*-python-pip``. Then install winrt
+bindings `pip install winrt-Windows.Foundation.Collections`,
+``winrt-Windows.Devices.Bluetooth.Advertisement`` and
+``winrt-Windows.Storage.Streams``.
If uxplay will be run with option "`uxplay -ble`" (so it writes data for the Bluetooth beacon in the default BLE data file
`~/.uxplay.ble`), just run ``uxplay-beacon.py`` in a separate terminal. The python script will start
@@ -1510,10 +1517,10 @@ Bluetooth LE Service-Discovery advertising when it detects that UxPlay is runnin
a running UxPlay plus this file (it will restart advertising if UxPlay later reappears). The script will remain active until stopped with Ctrl+C in its
terminal window (or its terminal window is closed).
-The beacon script can be more finely controlled using five possible options: these can be given on the command line, or read from
+The beacon script can be more finely controlled using certain options: these can be given on the command line, or read from
a configuration file `~/.uxplay.beacon`, if it exists. Configuration file entries are like the command line forms, one per line (e.g.,
`--ipv4 192.168.1.100`). Lines commented out with an initial ``#`` are ignored. Command line options override the configuration file
-options.
+options. Get help with `man uxplay-beacon` or ``uxplay-beacon.py --help``. Options are
* `--file ` read beacon options from ```` instead of
`~/.uxplay.beacon`.
@@ -1524,6 +1531,8 @@ it is not given, an address will be obtained automatically using `gethostbyname`
* `--path `. This overrides the default choice of BLE data file (``~/.uxplay.ble``) that is monitored by the beacon script. This also requires
that uxplay is run with option "`uxplay -ble `".
+The BlueZ/Dbus version has thee more options not offered by the Windows version:
+
* `--AdvMin x`, ``--AdvMax y``. These controls the interval between BLE advertisement broadcasts. This interval is in the range
[x, y], given in units of msecs. Allowed ranges are 100 <= x <= y <= 10240. If AdvMin=AdvMax, the interval is fixed: if AdvMin < AdvMax
it is chosen flexibly in this range to avoid interfering with other tasks the Bluetooth device is carrying out. The default values are
@@ -1542,12 +1551,14 @@ $ sudo systemctl stop avahi-daemon
To restore DNS_SD Service discovery, replace "mask" by "unmask", and "stop" by "start".
+On Windows, the Bonjour Service is controlled using **Services Management**: press "Windows + R" to open the Run dialog,
+run `services.msc`, and click on **Bonjour Service** in the alphabetic list. This
+will show links for it to be stopped and restarted.
For more information, see the [wiki page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon)
-This has useful information if you wish to build a python beacon controller script for Windows (we would like to have one!).
-* **Our current understanding is that Bluetooth LE AirPlay Service Discovery only supports
-broadcast of IPv4 addresses. Please let us know if this is incorrect, or if IPv6 support is introduced in the future.**
+* **Note that Bluetooth LE AirPlay Service Discovery only supports
+broadcast of IPv4 addresses**.
# Troubleshooting
Note: `uxplay` is run from a terminal command line, and informational
diff --git a/README.txt b/README.txt
index ed15a37..bdcc108 100644
--- a/README.txt
+++ b/README.txt
@@ -3,19 +3,20 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
- **NEW on github**: Support for **service discovery using a Bluetooth
- LE "beacon"** (as an alternative to Bonjour/Rendezvous DNS-SD
- service discovery). The user must set up a Bluetooth LE "beacon", (a
- USB 4.0 or later "dongle" can be used). See instructions below. The
- beacon runs independently of UxPlay and regularly broadcasts a
- Bluetooth LE ("Low Energy") 46 byte packet informing nearby
- iOS/macOS devices of the local IPv4 network address of the UxPlay
- server, and which TCP port to contact UxPlay on. A python script
- (Python \>=3.6) "uxplay-beacon.py", to broadcast the
- Service-Discovery advertisement will be installed on systems with
- DBus support (Linux and \*BSD, using BlueZ for Bluetooth control):
- this does **not** require enhanced "root permissions" to run. A
- windows version of this script is also planned for the future.
- Instructions are [given below](#bluetooth-le-beacon-setup).
+ LE "beacon"** for both Linux/\*BSD and Windows (as an alternative to
+ Bonjour/Rendezvous DNS-SD service discovery). The user must set up a
+ Bluetooth LE "beacon", (a USB 4.0 or later "dongle" can be used).
+ See instructions below. The beacon runs independently of UxPlay and
+ regularly broadcasts a Bluetooth LE ("Low Energy") 46 byte packet
+ informing nearby iOS/macOS devices of the local IPv4 network address
+ of the UxPlay server, and which TCP port to contact UxPlay on. Two
+ versions of a Python script (Python \>=3.6) "uxplay-beacon.py", (one
+ for Linux/\*BSD using BlueZ LEAdvertisingManager1 with DBus, and one
+ for Windows using winrt/BluetoothLEAdvertisementPublisher) are ready
+ for users to run: the appropriate version will be installed when
+ UxPlay is built. They independently run Service-Discovery beacons
+ that iOS devices respond to. Instructions are [given
+ below](#bluetooth-le-beacon-setup).
- **NEW on github**: option `-vrtp ` bypasses
rendering by UxPlay, and instead transmits rtp packets of decrypted
@@ -1516,20 +1517,27 @@ this to see even more of the GStreamer inner workings.
# Bluetooth LE beacon setup
The python\>=3.6 script for running a Bluetooth-LE Service Discovery
-beacon is uxplay-beacon.py. Currently only a DBus version (for Linux and
-\*BSD) is available, and it is only installed on systems which support
-DBus. Bluetooth \>= 4.0 hardware on the host computer is required: a
-cheap USB bluetooth dongle can be used. Bluetooth support (BlueZ) must
-be installed (on Debian-based systems:
-`sudo apt install bluez bluez-tools`; recent Ubuntu releases provide
-bluez as a snap package).
+beacon is uxplay-beacon.py. It comes in two versions, one (for Linux and
+\*BSD) is only installed on systems which support DBUS, and another only
+for Windows 10/11. Bluetooth \>= 4.0 hardware on the host computer is
+required: a cheap USB bluetooth dongle can be used.
-In addition to standard Python3 libraries, you may need to install the
-gi, dbus, and psutil Python libraries used by uxplay-beacon.py. On
-Debian-based systems:
+On Linux/\*BSD, Bluetooth support (BlueZ) must be installed (on
+Debian-based systems: `sudo apt install bluez bluez-tools`; recent
+Ubuntu releases provide bluez as a snap package). In addition to
+standard Python3 libraries, you may need to install the gi, dbus, and
+psutil Python libraries used by uxplay-beacon.py. On Debian-based
+systems:
sudo apt install python3-gi python3-dbus python3-psutil
+For Windows support on MSYS2 UCRT systems, use pacman -S to install
+`mingw-w64-ucrt-x86_64-python`, `*-python-gobject`, `*-python-psutil`,
+and `*-python-pip`. Then install winrt bindings
+`pip install winrt-Windows.Foundation.Collections`,
+`winrt-Windows.Devices.Bluetooth.Advertisement` and
+`winrt-Windows.Storage.Streams`.
+
If uxplay will be run with option "`uxplay -ble`" (so it writes data for
the Bluetooth beacon in the default BLE data file `~/.uxplay.ble`), just
run `uxplay-beacon.py` in a separate terminal. The python script will
@@ -1540,12 +1548,14 @@ advertising if UxPlay later reappears). The script will remain active
until stopped with Ctrl+C in its terminal window (or its terminal window
is closed).
-The beacon script can be more finely controlled using five possible
-options: these can be given on the command line, or read from a
-configuration file `~/.uxplay.beacon`, if it exists. Configuration file
-entries are like the command line forms, one per line (e.g.,
+The beacon script can be more finely controlled using certain options:
+these can be given on the command line, or read from a configuration
+file `~/.uxplay.beacon`, if it exists. Configuration file entries are
+like the command line forms, one per line (e.g.,
`--ipv4 192.168.1.100`). Lines commented out with an initial `#` are
ignored. Command line options override the configuration file options.
+Get help with `man uxplay-beacon` or `uxplay-beacon.py --help`. Options
+are
- `--file ` read beacon options from ``
instead of `~/.uxplay.beacon`.
@@ -1561,6 +1571,9 @@ ignored. Command line options override the configuration file options.
This also requires that uxplay is run with option
"`uxplay -ble `".
+The BlueZ/Dbus version has thee more options not offered by the Windows
+version:
+
- `--AdvMin x`, `--AdvMax y`. These controls the interval between BLE
advertisement broadcasts. This interval is in the range \[x, y\],
given in units of msecs. Allowed ranges are 100 \<= x \<= y
@@ -1587,15 +1600,16 @@ can disable DNS_SD Service discovery by the avahi-daemon with
To restore DNS_SD Service discovery, replace "mask" by "unmask", and
"stop" by "start".
-For more information, see the [wiki
-page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon) This has
-useful information if you wish to build a python beacon controller
-script for Windows (we would like to have one!).
+On Windows, the Bonjour Service is controlled using **Services
+Management**: press "Windows + R" to open the Run dialog, run
+`services.msc`, and click on **Bonjour Service** in the alphabetic list.
+This will show links for it to be stopped and restarted.
-- **Our current understanding is that Bluetooth LE AirPlay Service
- Discovery only supports broadcast of IPv4 addresses. Please let us
- know if this is incorrect, or if IPv6 support is introduced in the
- future.**
+For more information, see the [wiki
+page](https://github.com/FDH2/UxPlay/wiki/Bluetooth_LE_beacon)
+
+- **Note that Bluetooth LE AirPlay Service Discovery only supports
+ broadcast of IPv4 addresses**.
# Troubleshooting
From bdd82f621de1f3f961154d9eff23b140d90ce7df Mon Sep 17 00:00:00 2001
From: "F. Duncanh"
Date: Mon, 3 Nov 2025 02:40:57 -0500
Subject: [PATCH 58/58] python improvements
---
Bluetooth_LE_beacon/dbus/uxplay-beacon.py | 2 ++
Bluetooth_LE_beacon/winrt/uxplay-beacon.py | 2 ++
2 files changed, 4 insertions(+)
diff --git a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
index 9bb4deb..865c1ee 100644
--- a/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
+++ b/Bluetooth_LE_beacon/dbus/uxplay-beacon.py
@@ -265,9 +265,11 @@ def check_file_exists(file_path):
data = file.read(4)
pid = struct.unpack('