mirror of
https://github.com/morgan9e/UxPlay
synced 2026-04-14 00:04:13 +09:00
support for YouTube app HLS video language choice. also fix #452
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
33
lib/raop.c
33
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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user