diff --git a/README.html b/README.html
index 77cbffe..aedb5c7 100644
--- a/README.html
+++ b/README.html
@@ -57,9 +57,9 @@ alt="Current Packaging status" />.
Install uxplay on Debian-based Linux systems with
“sudo apt install uxplay”; on FreeBSD with
“sudo pkg install uxplay”; on OpenBSD with
-“doas pkg_add uxplay”. Also available on Arch-based
-systems through AUR. Since v. 1.66, uxplay is now also packaged in RPM
-format by Fedora 38 (“sudo dnf install uxplay”).
+“doas pkg_add uxplay”. Also available on Arch-based systems
+through AUR. Since v. 1.66, uxplay is now also packaged in RPM format by
+Fedora 38 (“sudo dnf install uxplay”).
For other RPM-based distributions which have not yet packaged
UxPlay, a RPM “specfile” uxplay.spec is now provided
with recent
OpenBSD: Install gstreamer1-libav,
-gstreamer1-plugins-* (* = core, bad, base, good). avahi-main must also
-be installed for the avahi_daemon rc startup script.
+gstreamer-plugins-* (* = core, bad, base, good). avahi-main must also be
+installed for the avahi_daemon rc startup script.
Starting and running UxPlay
Since UxPlay-1.64, UxPlay can be started with options read from a
@@ -1174,6 +1174,11 @@ then run the the image viewer in the foreground. Example, using
in which uxplay was put into the background). To quit, use
ctrl-C fg ctrl-C to terminate the image viewer, bring
uxplay into the foreground, and terminate it too.
+-md filename Like the -ca option, but
+exports audio metadata text (Artist, Title, Genre, etc.) to file for
+possible display by a process that watches the file for changes.
+Previous text is overwritten as new metadata is received, and the file
+is deleted when uxplay terminates.
-reset n sets a limit of n consecutive
failures of the client to send feedback requests (these “heartbeat
signals” are sent by the client once per second to ask for a response
@@ -1620,7 +1625,9 @@ introduced 2017, running tvOS 12.2.1), so it does not seem to matter
what version UxPlay claims to be.
Changelog
1.71 2024-12-13 Add support for HTTP Live Streaming (HLS), initially
-only for YouTube movies. Fix issue with NTP timeout on Windows.
+only for YouTube movies. Fix issue with NTP timeout on Windows. Add
+requested option -md <filename> to output audio metadata text to a
+file for possible display (complements -ca option).
1.70 2024-10-04 Add support for 4K (h265) video (resolution 3840 x
2160). Fix issue with GStreamer >= 1.24 when client sleeps, then
wakes.
diff --git a/README.md b/README.md
index 621cf19..5ee18f6 100644
--- a/README.md
+++ b/README.md
@@ -1195,6 +1195,11 @@ uxplay was put into the background). To quit, use `ctrl-C fg ctrl-C` to
terminate the image viewer, bring `uxplay` into the foreground, and
terminate it too.
+**-md *filename*** Like the -ca option, but exports audio metadata text
+(Artist, Title, Genre, etc.) to file for possible display by a process that watches
+the file for changes. Previous text is overwritten as new metadata is received,
+and the file is deleted when uxplay terminates.
+
**-reset n** sets a limit of *n* consecutive failures of the
client to send feedback requests (these "heartbeat signals" are sent by the client
once per second to ask for a response showing that the server is still online).
@@ -1666,6 +1671,8 @@ what version UxPlay claims to be.
1.71 2024-12-13 Add support for HTTP Live Streaming (HLS), initially
only for YouTube movies. Fix issue with NTP timeout on Windows.
+Add requested option -md \ to output audio metadata text to a file
+for possible display (complements -ca option).
1.70 2024-10-04 Add support for 4K (h265) video (resolution 3840 x
2160). Fix issue with GStreamer \>= 1.24 when client sleeps, then wakes.
diff --git a/README.txt b/README.txt
index 8eed072..fb71b6e 100644
--- a/README.txt
+++ b/README.txt
@@ -459,9 +459,9 @@ repositories for those distributions.
gstreamer1-plugins-\* (\* = core, good, bad, x, gtk, gl, vulkan,
pulse, v4l2, ...), (+ gstreamer1-vaapi for Intel/AMD graphics).
-- **OpenBSD:** Install gstreamer1-libav, gstreamer-plugins-\*
- (\* = core, bad, base, good). avahi-main must also be installed for
- the avahi_daemon rc startup script.
+- **OpenBSD:** Install gstreamer1-libav, gstreamer-plugins-\* (\* =
+ core, bad, base, good). avahi-main must also be installed for the
+ avahi_daemon rc startup script.
### Starting and running UxPlay
@@ -1196,6 +1196,11 @@ uxplay was put into the background). To quit, use `ctrl-C fg ctrl-C` to
terminate the image viewer, bring `uxplay` into the foreground, and
terminate it too.
+**-md *filename*** Like the -ca option, but exports audio metadata text
+(Artist, Title, Genre, etc.) to file for possible display by a process
+that watches the file for changes. Previous text is overwritten as new
+metadata is received, and the file is deleted when uxplay terminates.
+
**-reset n** sets a limit of *n* consecutive failures of the client to
send feedback requests (these "heartbeat signals" are sent by the client
once per second to ask for a response showing that the server is still
@@ -1665,7 +1670,9 @@ what version UxPlay claims to be.
# Changelog
1.71 2024-12-13 Add support for HTTP Live Streaming (HLS), initially
-only for YouTube movies. Fix issue with NTP timeout on Windows.
+only for YouTube movies. Fix issue with NTP timeout on Windows. Add
+requested option -md \ to output audio metadata text to a
+file for possible display (complements -ca option).
1.70 2024-10-04 Add support for 4K (h265) video (resolution 3840 x
2160). Fix issue with GStreamer \>= 1.24 when client sleeps, then wakes.
diff --git a/uxplay.1 b/uxplay.1
index 29b1179..6c930d2 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -108,6 +108,8 @@ UxPlay 1.71: An open\-source AirPlay mirroring (+ audio streaming) server:
.TP
\fB\-ca\fI fn \fR In Airplay Audio (ALAC) mode, write cover-art to file fn.
.TP
+\fB\-md\fI fn \fR In Airplay Audio (ALAC) mode, write metadata text to file fn.
+.TP
\fB\-reset\fR n Reset after n seconds client silence (default n=15, 0=never).
.TP
\fB\-nofreeze\fR Do NOT leave frozen screen in place after reset.
diff --git a/uxplay.cpp b/uxplay.cpp
index d66c34b..0fd3b60 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -125,6 +125,7 @@ static unsigned char audio_type = 0x00;
static unsigned char previous_audio_type = 0x00;
static bool fullscreen = false;
static std::string coverart_filename = "";
+static std::string metadata_filename = "";
static bool do_append_hostname = true;
static bool use_random_hw_addr = false;
static unsigned short display[5] = {0}, tcp[3] = {0}, udp[3] = {0};
@@ -232,6 +233,13 @@ static size_t write_coverart(const char *filename, const void *image, size_t len
return count;
}
+static size_t write_metadata(const char *filename, const char *text) {
+ FILE *fp = fopen(filename, "wb");
+ size_t count = fwrite(text, sizeof(char), strlen(text) + 1, fp);
+ fclose(fp);
+ return count;
+}
+
static char *create_pin_display(char *pin_str, int margin, int gap) {
char *ptr;
char num[2] = { 0 };
@@ -695,6 +703,7 @@ static void print_info (char *name) {
printf("-as 0 (or -a) Turn audio off, streamed video only\n");
printf("-al x Audio latency in seconds (default 0.25) reported to client.\n");
printf("-ca In Airplay Audio (ALAC) mode, write cover-art to file \n");
+ printf("-md In Airplay Audio (ALAC) mode, write metadata text to file \n");
printf("-reset n Reset after n seconds of client silence (default n=%d, 0=never)\n", MISSED_FEEDBACK_LIMIT);
printf("-nofreeze Do NOT leave frozen screen in place after reset\n");
printf("-nc Do NOT Close video window when client stops mirroring\n");
@@ -1137,6 +1146,19 @@ static void parse_arguments (int argc, char *argv[]) {
fprintf(stderr,"option -ca must be followed by a filename for cover-art output\n");
exit(1);
}
+ } else if (arg == "-md" ) {
+ if (option_has_value(i, argc, arg, argv[i+1])) {
+ metadata_filename.erase();
+ metadata_filename.append(argv[++i]);
+ const char *fn = metadata_filename.c_str();
+ if (!file_has_write_access(fn)) {
+ fprintf(stderr, "%s cannot be written to:\noption \"-ca \" must be to a file with write access\n", fn);
+ exit(1);
+ }
+ } else {
+ fprintf(stderr,"option -md must be followed by a filename for metadata text output\n");
+ exit(1);
+ }
} else if (arg == "-bt709") {
bt709_fix = true;
} else if (arg == "-srgb") {
@@ -1262,7 +1284,7 @@ static void parse_arguments (int argc, char *argv[]) {
}
}
-static void process_metadata(int count, const char *dmap_tag, const unsigned char* metadata, int datalen) {
+static void process_metadata(int count, const char *dmap_tag, const unsigned char* metadata, int datalen, std::string *metadata_text) {
int dmap_type = 0;
/* DMAP metadata items can be strings (dmap_type = 9); other types are byte, short, int, long, date, and list. *
* The DMAP item begins with a 4-character (4-letter) "dmap_tag" string that identifies the type. */
@@ -1284,13 +1306,13 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
case 'a':
switch (dmap_tag[3]) {
case 'a':
- printf("Album artist: "); /*asaa*/
+ metadata_text->append("Album artist: "); /*asaa*/
break;
case 'l':
- printf("Album: "); /*asal*/
+ metadata_text->append("Album: "); /*asal*/
break;
case 'r':
- printf("Artist: "); /*asar*/
+ metadata_text->append("Artist: "); /*asar*/
break;
default:
dmap_type = 0;
@@ -1300,16 +1322,16 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
case 'c':
switch (dmap_tag[3]) {
case 'm':
- printf("Comment: "); /*ascm*/
+ metadata_text->append("Comment: "); /*ascm*/
break;
case 'n':
- printf("Content description: "); /*ascn*/
+ metadata_text->append("Content description: "); /*ascn*/
break;
case 'p':
- printf("Composer: "); /*ascp*/
+ metadata_text->append("Composer: "); /*ascp*/
break;
case 't':
- printf("Category: "); /*asct*/
+ metadata_text->append("Category: "); /*asct*/
break;
default:
dmap_type = 0;
@@ -1319,22 +1341,22 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
case 's':
switch (dmap_tag[3]) {
case 'a':
- printf("Sort Artist: "); /*assa*/
+ metadata_text->append("Sort Artist: "); /*assa*/
break;
case 'c':
- printf("Sort Composer: "); /*assc*/
+ metadata_text->append("Sort Composer: "); /*assc*/
break;
case 'l':
- printf("Sort Album artist: "); /*assl*/
+ metadata_text->append("Sort Album artist: "); /*assl*/
break;
case 'n':
- printf("Sort Name: "); /*assn*/
+ metadata_text->append("Sort Name: "); /*assn*/
break;
case 's':
- printf("Sort Series: "); /*asss*/
+ metadata_text->append("Sort Series: "); /*asss*/
break;
case 'u':
- printf("Sort Album: "); /*assu*/
+ metadata_text->append("Sort Album: "); /*assu*/
break;
default:
dmap_type = 0;
@@ -1343,15 +1365,15 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
break;
default:
if (strcmp(dmap_tag, "asdt") == 0) {
- printf("Description: ");
+ metadata_text->append("Description: ");
} else if (strcmp (dmap_tag, "asfm") == 0) {
- printf("Format: ");
+ metadata_text->append("Format: ");
} else if (strcmp (dmap_tag, "asgn") == 0) {
- printf("Genre: ");
+ metadata_text->append("Genre: ");
} else if (strcmp (dmap_tag, "asky") == 0) {
- printf("Keywords: ");
+ metadata_text->append("Keywords: ");
} else if (strcmp (dmap_tag, "aslc") == 0) {
- printf("Long Content Description: ");
+ metadata_text->append("Long Content Description: ");
} else {
dmap_type = 0;
}
@@ -1359,21 +1381,27 @@ static void process_metadata(int count, const char *dmap_tag, const unsigned cha
}
} else if (strcmp (dmap_tag, "minm") == 0) {
dmap_type = 9;
- printf("Title: ");
+ metadata_text->append("Title: ");
}
if (dmap_type == 9) {
- char *str = (char *) calloc(1, datalen + 1);
+ char *str = (char *) calloc(datalen + 1, sizeof(char));
memcpy(str, metadata, datalen);
- printf("%s", str);
+ metadata_text->append(str);
+ metadata_text->append("\n");
free(str);
} else if (debug_log) {
+ std::string md = "";
+ char hex[4];
for (int i = 0; i < datalen; i++) {
- if (i > 0 && i % 16 == 0) printf("\n");
- printf("%2.2x ", (int) metadata[i]);
+ if (i > 0 && i % 16 == 0) {
+ md.append("\n");
+ }
+ snprintf(hex, 4, "%2.2x ", (int) metadata[i]);
+ md.append(hex);
}
+ LOGI("%s", md.c_str());
}
- printf("\n");
}
static int parse_dmap_header(const unsigned char *metadata, char *tag, int *len) {
@@ -1833,6 +1861,9 @@ extern "C" void audio_get_format (void *cls, unsigned char *ct, unsigned short *
if (coverart_filename.length()) {
write_coverart(coverart_filename.c_str(), (const void *) empty_image, sizeof(empty_image));
}
+ if (metadata_filename.length()) {
+ write_metadata(metadata_filename.c_str(), "no data\n");
+ }
}
extern "C" void video_report_size(void *cls, float *width_source, float *height_source, float *width, float *height) {
@@ -1879,6 +1910,7 @@ extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
dmap_tag, datalen, buflen);
return;
}
+ std::string metadata_text = "";
while (buflen >= 8) {
count++;
if (parse_dmap_header(metadata, dmap_tag, &datalen)) {
@@ -1887,12 +1919,16 @@ extern "C" void audio_set_metadata(void *cls, const void *buffer, int buflen) {
}
metadata += 8;
buflen -= 8;
- process_metadata(count, (const char *) dmap_tag, metadata, datalen);
+ process_metadata(count, (const char *) dmap_tag, metadata, datalen, &metadata_text);
metadata += datalen;
buflen -= datalen;
}
+ LOGI("%s", metadata_text.c_str());
+ if (metadata_filename.length()) {
+ write_metadata(metadata_filename.c_str(), metadata_text.c_str());
+ }
if (buflen != 0) {
- LOGE("%d bytes of metadata were not processed", buflen);
+ LOGE("%d bytes of metadata were not processed", buflen);
}
}
@@ -2381,6 +2417,11 @@ int main (int argc, char *argv[]) {
write_coverart(coverart_filename.c_str(), (const void *) empty_image, sizeof(empty_image));
}
+ if (metadata_filename.length()) {
+ LOGI("any AirPlay audio metadata text will be written to file %s",metadata_filename.c_str());
+ write_metadata(metadata_filename.c_str(), "no data\n");
+ }
+
/* set default resolutions for h264 or h265*/
if (!display[0] && !display[1]) {
if (h265_support) {
@@ -2459,4 +2500,7 @@ int main (int argc, char *argv[]) {
if (coverart_filename.length()) {
remove (coverart_filename.c_str());
}
+ if (metadata_filename.length()) {
+ remove (metadata_filename.c_str());
+ }
}