From 47120552c468a1ba29f8147537460500361a43eb Mon Sep 17 00:00:00 2001 From: "F. Duncanh" Date: Wed, 16 Apr 2025 19:07:44 -0400 Subject: [PATCH] add -md option to output audio-mode metadata text to file --- README.html | 19 +++++++---- README.md | 7 ++++ README.txt | 15 +++++--- uxplay.1 | 2 ++ uxplay.cpp | 98 ++++++++++++++++++++++++++++++++++++++--------------- 5 files changed, 104 insertions(+), 37 deletions(-) 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()); + } }