support for YouTube app HLS video language choice. also fix #452

This commit is contained in:
F. Duncanh
2025-11-09 17:22:05 -05:00
parent 940d3286eb
commit 1072692172
11 changed files with 311 additions and 21 deletions

View File

@@ -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) {

View File

@@ -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");

View File

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

View File

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

View File

@@ -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);

View File

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