add -md <file> option to output audio-mode metadata text to file

This commit is contained in:
F. Duncanh
2025-04-16 19:07:44 -04:00
parent c51b37c71d
commit 47120552c4
5 changed files with 104 additions and 37 deletions

View File

@@ -57,9 +57,9 @@ alt="Current Packaging status" /></a>.</p>
<li><p>Install uxplay on Debian-based Linux systems with
<code>sudo apt install uxplay</code>”; on FreeBSD with
<code>sudo pkg install uxplay</code>”; on OpenBSD with
<code>doas pkg_add uxplay</code>”. Also available on Arch-based
systems through AUR. Since v. 1.66, uxplay is now also packaged in RPM
format by Fedora 38 (“<code>sudo dnf install uxplay</code>”).</p></li>
<code>doas pkg_add uxplay</code>”. Also available on Arch-based systems
through AUR. Since v. 1.66, uxplay is now also packaged in RPM format by
Fedora 38 (“<code>sudo dnf install uxplay</code>”).</p></li>
<li><p>For other RPM-based distributions which have not yet packaged
UxPlay, a RPM “specfile” <strong>uxplay.spec</strong> is now provided
with recent <a
@@ -467,8 +467,8 @@ gstreamer1-plugins, gstreamer1-plugins-* (* = core, good, bad, x, gtk,
gl, vulkan, pulse, v4l2, …), (+ gstreamer1-vaapi for Intel/AMD
graphics).</p></li>
<li><p><strong>OpenBSD:</strong> Install gstreamer1-libav,
gstreamer1-plugins-* (* = core, bad, base, good). avahi-main must also
be installed for the avahi_daemon rc startup script.</p></li>
gstreamer-plugins-* (* = core, bad, base, good). avahi-main must also be
installed for the avahi_daemon rc startup script.</p></li>
</ul>
<h3 id="starting-and-running-uxplay">Starting and running UxPlay</h3>
<p>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
<code>ctrl-C fg ctrl-C</code> to terminate the image viewer, bring
<code>uxplay</code> into the foreground, and terminate it too.</p>
<p><strong>-md <em>filename</em></strong> 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.</p>
<p><strong>-reset n</strong> sets a limit of <em>n</em> 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.</p>
<h1 id="changelog">Changelog</h1>
<p>1.71 2024-12-13 Add support for HTTP Live Streaming (HLS), initially
only for YouTube movies. Fix issue with NTP timeout on Windows.</p>
only for YouTube movies. Fix issue with NTP timeout on Windows. Add
requested option -md &lt;filename&gt; to output audio metadata text to a
file for possible display (complements -ca option).</p>
<p>1.70 2024-10-04 Add support for 4K (h265) video (resolution 3840 x
2160). Fix issue with GStreamer &gt;= 1.24 when client sleeps, then
wakes.</p>

View File

@@ -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 \<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.

View File

@@ -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 \<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.

View File

@@ -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.

View File

@@ -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 <fn> In Airplay Audio (ALAC) mode, write cover-art to file <fn>\n");
printf("-md <fn> In Airplay Audio (ALAC) mode, write metadata text to file <fn>\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 <fn>\" 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());
}
}