NEW on github: Support for service
discovery using a Bluetooth LE “beacon” for both Linux/*BSD and
@@ -1115,6 +1122,14 @@ 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.)
+-lang [list] Specify language preferences for
+YouTube app HLS videos, which may offer a choice of language (based on
+AI dubbing). If this option is not used, preferences will be taken from
+environment variable $LANGUAGE, if set. Both methods specify the
+preference order as e.g. list = fr:es:en, fot French (first
+choice), Spanish (second choice), and English (third choice).
+If “list” is not given (or list = 0), $LANGUAGE is ignored and undubbed
+audio is played.
-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
@@ -1911,7 +1926,9 @@ 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). Check that option input strings have valid UTF-8 encoding.
+Studio). Check that option input strings have valid UTF-8 encoding. New
+option -lang fr:es:en to specify language preferences for
+YouTube HLS videos when they offer a choice.
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 23dfbfb..a8d88c0 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,10 @@
### **Now developed at the GitHub site (where ALL user issues should be posted, and latest versions can be found).**
+-- **NEW on github**: some YouTube app HLS videos now offer alternative language tracks (generated by AI dubbing). Language choices will be made in order of
+ preferences set with option -lang (or by environment variable $LANGUAGE, which it overrides). Format is `-lang fr:es:en`, where French ("fr") is
+ the first choice, if available, then Spanish ("es"), etc. $LANGUAGE has the same format. `-lang` by itself suppresses playing of dubbed audio.
+
- **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
@@ -1103,6 +1107,14 @@ 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.)_
+**-lang \[list\]** Specify language preferences for YouTube app HLS videos,
+which may offer a choice of language (based on AI dubbing). If this option is not
+used, preferences will be taken from environment variable $LANGUAGE, if set. Both
+methods specify the preference order as e.g. list = `fr:es:en`, fot French (first
+choice), Spanish (second choice), and English (third choice).
+If "list" is not given (or list = 0), $LANGUAGE is ignored and undubbed audio is played.
+
+
**-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
@@ -1927,7 +1939,8 @@ 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). Check that option input strings
-have valid UTF-8 encoding.
+have valid UTF-8 encoding. New option `-lang fr:es:en` to specify language
+preferences for YouTube HLS videos when they offer a choice.
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 5bc412b..9a55967 100644
--- a/README.txt
+++ b/README.txt
@@ -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**: some YouTube app HLS videos now offer alternative
+language tracks (generated by AI dubbing). Language choices will be made
+in order of preferences set with option -lang (or by environment
+variable \$LANGUAGE, which it overrides). Format is `-lang fr:es:en`,
+where French ("fr") is the first choice, if available, then Spanish
+("es"), etc. \$LANGUAGE has the same format. `-lang` by itself
+suppresses playing of dubbed audio.
+
- **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
@@ -1136,6 +1144,15 @@ 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.)*
+**-lang \[list\]** Specify language preferences for YouTube app HLS
+videos, which may offer a choice of language (based on AI dubbing). If
+this option is not used, preferences will be taken from environment
+variable \$LANGUAGE, if set. Both methods specify the preference order
+as e.g. list = `fr:es:en`, fot French (first choice), Spanish (second
+choice), and English (third choice).\
+If "list" is not given (or list = 0), \$LANGUAGE is ignored and undubbed
+audio is played.
+
**-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
@@ -1979,7 +1996,9 @@ 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). Check that option input strings have valid UTF-8 encoding.
+Studio). Check that option input strings have valid UTF-8 encoding. New
+option `-lang fr:es:en` to specify language preferences for YouTube HLS
+videos when they offer a choice.
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/airplay_video.c b/lib/airplay_video.c
index dfcea64..44738d1 100644
--- a/lib/airplay_video.c
+++ b/lib/airplay_video.c
@@ -37,6 +37,7 @@ struct airplay_video_s {
char apple_session_id[37];
char playback_uuid[37];
char *uri_prefix;
+ const char *lang;
char local_uri_prefix[23];
int next_uri;
int FCUP_RequestID;
@@ -52,7 +53,7 @@ struct airplay_video_s {
// initialize airplay_video service.
int airplay_video_service_init(raop_t *raop, unsigned short http_port,
- const char *session_id) {
+ const char *lang, const char *session_id) {
char uri[] = "http://localhost:xxxxx";
assert(raop);
@@ -68,7 +69,8 @@ int airplay_video_service_init(raop_t *raop, unsigned short http_port,
return -1;
}
- /* create local_uri_prefix string */
+ airplay_video->lang = lang;
+ /* create local_uri_prefix string */
strncpy(airplay_video->local_uri_prefix, uri, sizeof(airplay_video->local_uri_prefix));
char *ptr = strstr(airplay_video->local_uri_prefix, "xxxxx");
snprintf(ptr, 6, "%-5u", http_port);
@@ -176,11 +178,186 @@ int get_next_media_uri_id(airplay_video_t *airplay_video) {
return airplay_video->next_uri;
}
+typedef struct language_s {
+ char *start;
+ int len;
+ char type;
+ char code[6];
+} language_t;
+
+language_t* master_playlist_process_language(char * data, int *slices, int *language_count) {
+ *language_count = 0;
+ char *ptr = data;
+ int count = 0, count1 = 0, count2 = 0, count3 = 0;
+ while (ptr) {
+ ptr = strstr(ptr,"#EXT-X-MEDIA:URI=");
+ if(!ptr) {
+ break;
+ }
+ ptr = strstr(ptr, "LANGUAGE=");
+ if(!ptr) {
+ break;
+ }
+ ptr = strstr(ptr,"YT-EXT-AUDIO-CONTENT-ID=");
+ if(!ptr) {
+ break;
+ }
+ count++;
+ }
+ if (count == 0) {
+ return NULL;
+ }
+ language_t *languages = (language_t *) calloc(count + 2, sizeof(language_t));
+ languages[0].start = data;
+ ptr = data;
+ for (int i = 1; i <= count; i++) {
+ char *end;
+ if (!(ptr = strstr(ptr, "#EXT-X-MEDIA"))) {
+ break;
+ }
+ count1++;
+ if (i == 1) {
+ languages[0].len = (int) (ptr - data);
+ languages[0].type = ' ';
+ }
+ languages[i].start = ptr;
+ if (!(ptr = strstr(ptr, "LANGUAGE="))) {
+ break;
+ }
+ if (!strncmp(ptr - strlen("dubbed-auto") - 2, "dubbed-auto", strlen("dubbed-auto"))) {
+ languages[i].type = 'd';
+ } else if (!strncmp(ptr - strlen("original") - 2, "original", strlen("original"))) {
+ languages[i].type = 'o';
+ } else {
+ languages[i].type = 'u';
+ }
+ count2++;
+ if (!(ptr = strchr(ptr,'"'))) {
+ break;
+ }
+ ptr++;
+ if (!(end = strchr(ptr,'"'))) {
+ break;
+ }
+ strncpy(languages[i].code, ptr, end - ptr);
+ if (!(ptr = strchr(ptr,'\n'))) {
+ break;
+ }
+ count3++;
+ languages[i].len = (int) (ptr + 1 - languages[i].start);
+ }
+ assert (count1 == count && count2 == count && count3 == count);
+ languages[count + 1].start = ++ptr;
+ languages[count + 1].len = strlen(ptr);
+ languages[count + 1].type = ' ';
+ *slices = count + 2;
+ int len = 0;
+ int copies = 0;
+ for (int i = 0; i < *slices; i++) {
+ if (!strcmp(languages[i].code, languages[1].code)) {
+ copies++;
+ }
+ len += languages[i].len;
+ }
+ if (copies == count) {
+ /* only one language is offered, nothing to do */
+ free (languages);
+ return NULL;
+ }
+
+ *language_count = count/copies;
+ assert(count == *language_count * copies);
+ assert(len == (int) strlen(data));
+ /* verify expected structure of language choice information */
+ for (int i = 1; i <= count; i++) {
+ if (i % *language_count) {
+ assert(languages[i].type == 'd');
+ } else {
+ assert(languages[i].type == 'o');
+ }
+ int j = i - *language_count;
+ if (j > 0) {
+ assert (!strcmp(languages[i].code, languages[j].code));
+ }
+ }
+ return languages;
+}
+
void store_master_playlist(airplay_video_t *airplay_video, char *master_playlist) {
+ int language_count, slices;
if (airplay_video->master_playlist) {
free (airplay_video->master_playlist);
}
airplay_video->master_playlist = master_playlist;
+ language_t *languages;
+ if (!(languages = master_playlist_process_language(airplay_video->master_playlist,
+ &slices, &language_count))) {
+ return;
+ }
+ /* audio is offered in multiple languages */
+ char *str = calloc(6 * language_count, sizeof(char));
+ int i;
+ char *ptr = str;
+ for (i = 0; i < language_count; i++) {
+ sprintf(ptr,"%s ", languages[i + 1].code);
+ ptr += strlen(languages[i + 1].code);
+ ptr++;
+ if ( i % 16 == 15) {
+ sprintf(ptr++,"\n");
+ }
+ }
+ if (i % 16 != 15) {
+ sprintf(ptr++,"\n");
+ }
+ printf("%d available languages: %s", language_count, str);
+ free(str);
+
+ const char *ptrc = airplay_video->lang;
+ char *lang = NULL;
+ while (ptrc) {
+ for (int i = 1; i <= language_count; i++) {
+ if (!strncmp(languages[i].code, ptrc, 2)) {
+ lang = languages[i].code;
+ break;
+ }
+ }
+ if (lang) {
+ break;
+ }
+ ptrc = strchr(ptrc,':');
+ if(ptrc) {
+ ptrc++;
+ if (strlen(ptrc) < 2) {
+ break;
+ }
+ }
+ }
+ if (lang) {
+ printf("language choice: %s (based on prefered languages list %s)\n\n",
+ lang, airplay_video->lang);
+ } else {
+ if (airplay_video->lang) {
+ printf("no match with prefered language list %s\n", airplay_video->lang);
+ }
+ lang = languages[language_count].code;
+ printf("default language choice: %s\n\n", lang);
+ }
+ int len = 0;
+ for (int i = 0; i < slices; i++) {
+ if (strlen(languages[i].code) == 0 || !strcmp(languages[i].code, lang)) {
+ len += languages[i].len;
+ }
+ }
+ airplay_video->master_playlist = (char *) calloc(len + 1, sizeof(char));
+ ptr = airplay_video->master_playlist;
+ for (int i = 0; i < slices; i++) {
+ if (strlen(languages[i].code) == 0 || !strcmp(languages[i].code, lang)) {
+ strncpy(ptr, languages[i].start, languages[i].len);
+ ptr += languages[i].len;
+ }
+ }
+ free (languages);
+ free(master_playlist);
}
char *get_master_playlist(airplay_video_t *airplay_video) {
diff --git a/lib/http_handlers.h b/lib/http_handlers.h
index 93c65c9..11aeba3 100644
--- a/lib/http_handlers.h
+++ b/lib/http_handlers.h
@@ -84,7 +84,7 @@ http_handler_server_info(raop_conn_t *conn, http_request_t *request, http_respon
/* initialize the airplay video service */
const char *session_id = http_request_get_header(request, "X-Apple-Session-ID");
- airplay_video_service_init(conn->raop, conn->raop->port, session_id);
+ airplay_video_service_init(conn->raop, conn->raop->port, conn->raop->lang, session_id);
}
@@ -289,7 +289,7 @@ http_handler_playback_info(raop_conn_t *conn, http_request_t *request, http_resp
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);
+ conn->raop->callbacks.video_reset(conn->raop->callbacks.cls, true);
return;
} else if (playback_info.position == -1.0) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available");
@@ -442,9 +442,9 @@ http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t
}
plist_mem_free (remove_uuid);
}
- logger_log(conn->raop->logger, LOGGER_ERR, "FIXME: playlist removal not yet implemented");
goto finish;
} else if (playlist_insert) {
+ logger_log(conn->raop->logger, LOGGER_ERR, "FIXME: playlist insertion not yet implemented");
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");
diff --git a/lib/raop.c b/lib/raop.c
index ee0c5e4..82d360c 100644
--- a/lib/raop.c
+++ b/lib/raop.c
@@ -82,6 +82,9 @@ struct raop_s {
char *nonce;
char *random_pw;
unsigned char auth_fail_count;
+
+ /* used for setting HLS video language choices */
+ char *lang;
};
struct raop_conn_s {
@@ -233,7 +236,7 @@ conn_request(void *ptr, http_request_t *request, http_response_t **response) {
if (httpd_nohold(conn->raop->httpd)) {
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);
+ conn->raop->callbacks.video_reset(conn->raop->callbacks.cls, false);
}
httpd_remove_known_connections(conn->raop->httpd);
} else {
@@ -594,6 +597,8 @@ raop_init(raop_callbacks_t *callbacks) {
raop->hls_support = false;
raop->nonce = NULL;
+
+ raop->lang = NULL;
return raop;
}
@@ -660,6 +665,10 @@ raop_destroy(raop_t *raop) {
free(raop->random_pw);
}
+ if (raop->lang) {
+ free(raop->lang);
+ }
+
free(raop);
/* Cleanup the network */
@@ -767,6 +776,22 @@ raop_set_dnssd(raop_t *raop, dnssd_t *dnssd) {
raop->dnssd = dnssd;
}
+void
+raop_set_lang(raop_t *raop, const char *lang) {
+ if (raop->lang) {
+ free (raop->lang);
+ raop->lang = NULL;
+ }
+ if (lang && strlen(lang)) {
+ raop->lang = (char *) calloc(strlen(lang) + 1, sizeof(char));
+ memcpy(raop->lang, lang, strlen(lang) * sizeof(char));
+ }
+}
+
+char *
+raop_get_lang(raop_t *raop) {
+ return raop->lang;
+}
int
raop_start_httpd(raop_t *raop, unsigned short *port) {
@@ -785,6 +810,12 @@ void raop_remove_known_connections(raop_t * raop) {
httpd_remove_known_connections(raop->httpd);
}
+void raop_remove_hls_connections(raop_t * raop) {
+ httpd_remove_connections_by_type(raop->httpd, CONNECTION_TYPE_HLS);
+ httpd_remove_connections_by_type(raop->httpd, CONNECTION_TYPE_PTTH);
+ httpd_remove_connections_by_type(raop->httpd, CONNECTION_TYPE_AIRPLAY);
+}
+
airplay_video_t *deregister_airplay_video(raop_t *raop) {
airplay_video_t *airplay_video = raop->airplay_video;
raop->airplay_video = NULL;
diff --git a/lib/raop.h b/lib/raop.h
index 3eb1054..d1581a8 100644
--- a/lib/raop.h
+++ b/lib/raop.h
@@ -69,7 +69,7 @@ struct raop_callbacks_s {
void (*video_resume)(void *cls);
void (*conn_feedback) (void *cls);
void (*conn_reset) (void *cls, int reason);
- void (*video_reset) (void *cls);
+ void (*video_reset) (void *cls, bool hls_shutdown);
/* Optional but recommended callback functions (probably not optional, check this)*/
@@ -108,9 +108,9 @@ raop_ntp_t *raop_ntp_init(logger_t *logger, raop_callbacks_t *callbacks, const c
int remote_addr_len, unsigned short timing_rport,
timing_protocol_t *time_protocol);
-int airplay_video_service_init(raop_t *raop, unsigned short port, const char *session_id);
-
+int airplay_video_service_init(raop_t *raop, unsigned short port, const char *lang, const char *session_id);
bool register_airplay_video(raop_t *raop, airplay_video_t *airplay_video);
+char *raop_get_lang(raop_t *raop);
airplay_video_t *get_airplay_video(raop_t *raop);
airplay_video_t *deregister_airplay_video(raop_t *raop);
uint64_t get_local_time();
@@ -121,6 +121,7 @@ RAOP_API void raop_set_log_level(raop_t *raop, int level);
RAOP_API void raop_set_log_callback(raop_t *raop, raop_log_callback_t callback, void *cls);
RAOP_API int raop_set_plist(raop_t *raop, const char *plist_item, const int value);
RAOP_API void raop_set_port(raop_t *raop, unsigned short port);
+RAOP_API void raop_set_lang(raop_t *raop, const char *lang);
RAOP_API void raop_set_udp_ports(raop_t *raop, unsigned short port[3]);
RAOP_API void raop_set_tcp_ports(raop_t *raop, unsigned short port[2]);
RAOP_API unsigned short raop_get_port(raop_t *raop);
@@ -131,6 +132,7 @@ RAOP_API void raop_stop_httpd(raop_t *raop);
RAOP_API void raop_set_dnssd(raop_t *raop, dnssd_t *dnssd);
RAOP_API void raop_destroy(raop_t *raop);
RAOP_API void raop_remove_known_connections(raop_t * raop);
+RAOP_API void raop_remove_hls_connections(raop_t * raop);
RAOP_API void raop_destroy_airplay_video(raop_t *raop);
#ifdef __cplusplus
diff --git a/lib/raop_handlers.h b/lib/raop_handlers.h
index 1e1bd8b..67b0c49 100644
--- a/lib/raop_handlers.h
+++ b/lib/raop_handlers.h
@@ -1268,7 +1268,7 @@ raop_handler_teardown(raop_conn_t *conn,
}
}
} else if (teardown_110) {
- conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
+ conn->raop->callbacks.video_reset(conn->raop->callbacks.cls, false);
if (conn->raop_rtp_mirror) {
/* Stop our video RTP session */
raop_rtp_mirror_stop(conn->raop_rtp_mirror);
@@ -1284,7 +1284,10 @@ raop_handler_teardown(raop_conn_t *conn,
conn->raop_rtp_mirror = NULL;
}
/* shut down any HLS connections */
- httpd_remove_connections_by_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
+ int hls_count = httpd_count_connection_type(conn->raop->httpd, CONNECTION_TYPE_HLS);
+ if (hls_count) {
+ conn->raop->callbacks.video_reset(conn->raop->callbacks.cls, true);
+ }
}
if (conn->raop->callbacks.conn_teardown) {
conn->raop->callbacks.conn_teardown(conn->raop->callbacks.cls, &teardown_96, &teardown_110);
diff --git a/lib/raop_rtp_mirror.c b/lib/raop_rtp_mirror.c
index 1ca548d..0fb253e 100644
--- a/lib/raop_rtp_mirror.c
+++ b/lib/raop_rtp_mirror.c
@@ -863,7 +863,7 @@ raop_rtp_mirror_thread(void *arg)
if (unsupported_codec) {
closesocket(raop_rtp_mirror->mirror_data_sock);
raop_rtp_mirror_stop(raop_rtp_mirror);
- raop_rtp_mirror->callbacks.video_reset(raop_rtp_mirror->callbacks.cls);
+ raop_rtp_mirror->callbacks.video_reset(raop_rtp_mirror->callbacks.cls, false);
}
return 0;
diff --git a/uxplay.1 b/uxplay.1
index dba778b..4262660 100644
--- a/uxplay.1
+++ b/uxplay.1
@@ -15,10 +15,14 @@ UxPlay 1.72: An open\-source AirPlay mirroring (+ audio streaming) server:
.TP
\fB\-h265\fR Support h265 (4K) video (with h265 versions of h264 plugins)
.TP
-\fB\-hls\fR Support HTTP Live Streaming (currently YouTube video only)
+\fB\-hls\fR [v] Support HTTP Live Streaming (HLS), YouTube app video only:
.IP
v = 2 or 3 (default 3) optionally selects video player version
.TP
+\fB\-lang\fR xx HLS language preferences ("fr:es:..", overrides $LANGUAGE)
+.TP
+\fB\-lang\fR (or -lang 0): play undubbed HLS version (overrides $LANGUAGE)
+.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)
diff --git a/uxplay.cpp b/uxplay.cpp
index 103f1f4..937ae81 100644
--- a/uxplay.cpp
+++ b/uxplay.cpp
@@ -175,6 +175,7 @@ static bool h265_support = false;
static int n_video_renderers = 0;
static int n_audio_renderers = 0;
static bool hls_support = false;
+static std::string lang = "";
static std::string url = "";
static guint gst_x11_window_id = 0;
static guint progress_id = 0;
@@ -880,8 +881,10 @@ static void print_info (char *name) {
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");
+ printf("-hls [v] Support HTTP Live Streaming (HLS), Youtube app video only: \n");
printf(" v = 2 or 3 (default 3) optionally selects video player version\n");
+ printf("-lang xx HLS language preferences (\"fr:es:..\", overrides $LANGUAGE)\n");
+ printf("-lang (or -lang 0): play undubbed HLS version (overrides $LANGUAGE)\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");
@@ -1666,7 +1669,12 @@ static void parse_arguments (int argc, char *argv[]) {
exit(1);
}
playbin_version = (guint) n;
- }
+ }
+ } else if (arg == "-lang") {
+ lang.erase();
+ if (i < argc - 1 && *argv[i+1] != '-') {
+ lang = argv[++i];
+ }
} else if (arg == "-h265") {
h265_support = true;
} else if (arg == "-nofreeze") {
@@ -2035,10 +2043,15 @@ static bool check_blocked_client(char *deviceid) {
// Server callbacks
-extern "C" void video_reset(void *cls) {
+extern "C" void video_reset(void *cls, bool hls_shutdown) {
LOGD("video_reset");
video_renderer_stop();
- url.erase();
+ if (hls_shutdown) {
+ url.erase();
+ raop_destroy_airplay_video(raop);
+ raop_remove_hls_connections(raop);
+ preserve_connections = true;
+ }
remote_clock_offset = 0;
relaunch_video = true;
reset_loop = true;
@@ -2356,7 +2369,7 @@ extern "C" void audio_set_coverart(void *cls, const void *buffer, int buflen) {
extern "C" void audio_stop_coverart_rendering(void *cls) {
if (render_coverart) {
- video_reset(cls);
+ video_reset(cls, false);
}
}
@@ -2731,6 +2744,13 @@ int main (int argc, char *argv[]) {
if (!getenv("AVAHI_COMPAT_NOWARN")) putenv(avahi_compat_nowarn);
#endif
+ /* for HLS video language preferences */
+ char *lang_env = getenv("LANGUAGE");
+ if (lang_env && strlen(lang_env)) {
+ lang.erase();
+ lang = lang_env;
+ }
+
char *rcfile = NULL;
/* see if option -rc was given */
for (int i = 1; i < argc ; i++) {
@@ -3018,6 +3038,10 @@ int main (int argc, char *argv[]) {
cleanup();
}
+ if (lang.length() > 1) {
+ raop_set_lang(raop, lang.c_str());
+ }
+
#define PID_MAX 4194304 // 2^22
if (ble_filename.length()) {
#ifdef _WIN32