Permalink
| #include <string.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <stdint.h> | |
| #include <assert.h> | |
| #include <pthread.h> | |
| #include <time.h> | |
| #ifndef _MSC_VER | |
| #if !defined(__APPLE__) && !defined(__MINGW32__) && !defined(__CYGWIN__) | |
| #include <sys/prctl.h> | |
| #endif | |
| #include <unistd.h> | |
| #else | |
| #include <Windows.h> | |
| #define sleep Sleep | |
| #endif | |
| #include <inttypes.h> | |
| #include "curl.h" | |
| #include "hls.h" | |
| #include "msg.h" | |
| #include "misc.h" | |
| #include "aes.h" | |
| #include "mpegts.h" | |
| static uint64_t get_duration_ms(const char *ptr) | |
| { | |
| uint64_t v1 = 0; | |
| uint64_t v2 = 0; | |
| uint32_t n = 0; | |
| bool hasDot = false; | |
| while (*ptr == ' ' || *ptr == '\t' ) ++ptr; | |
| while (*ptr != '\0') { | |
| if (*ptr >= '0' && *ptr <= '9') { | |
| uint32_t digit = (uint32_t)((*ptr) - '0'); | |
| if (!hasDot) | |
| v1 = v1 * 10 + digit; | |
| else if (n < 3) { | |
| ++n; | |
| v2 = v2 * 10 + digit; | |
| } | |
| else | |
| break; | |
| } | |
| else if (*ptr == '.' && !hasDot) { | |
| hasDot = true; | |
| } | |
| else | |
| break; | |
| ++ptr; | |
| } | |
| if (v2 > 0) | |
| while (n < 3) { | |
| ++n; | |
| v2 *= 10; | |
| } | |
| return v1 * 1000 + v2; | |
| } | |
| static void set_hls_http_header(void *session) | |
| { | |
| if (hls_args.user_agent) { | |
| set_user_agent_http_session(session, hls_args.user_agent); | |
| } | |
| if (hls_args.proxy_uri) { | |
| set_proxy_uri_http_session(session, hls_args.proxy_uri); | |
| } | |
| if (hls_args.cookie_file) { | |
| set_cookie_file_session(session, hls_args.cookie_file, hls_args.cookie_file_mutex); | |
| } | |
| for (int i=0; i<HLSDL_MAX_NUM_OF_CUSTOM_HEADERS; ++i) { | |
| if (hls_args.custom_headers[i]) { | |
| add_custom_header_http_session(session, hls_args.custom_headers[i]); | |
| } | |
| else { | |
| break; | |
| } | |
| } | |
| } | |
| static void * init_hls_session(void) | |
| { | |
| void *session = init_http_session(); | |
| assert(session); | |
| set_hls_http_header(session); | |
| return session; | |
| } | |
| long get_hls_data_from_url(char *url, char **out, size_t *size, int type, char **new_url) | |
| { | |
| void *session = init_hls_session(); | |
| long http_code = get_data_from_url_with_session(&session, url, out, size, type, new_url, -1, -1); | |
| clean_http_session(session); | |
| return http_code; | |
| } | |
| int is_playlist_FPS(char* source) | |
| { | |
| return strstr(source, "KEYFORMAT=\"com.apple.streamingkeydelivery\""); | |
| } | |
| int get_playlist_type(char *source) | |
| { | |
| if (strncmp("#EXTM3U", source, 7) != 0) { | |
| MSG_WARNING("Not a valid M3U8 file. Exiting.\n"); | |
| return -1; | |
| } | |
| if (strstr(source, "#EXT-X-STREAM-INF")) { | |
| return MASTER_PLAYLIST; | |
| } | |
| if (!hls_args.force_ignoredrm && is_playlist_FPS(source)) { | |
| MSG_WARNING("HLS stream is DRM protected. Exiting\n"); | |
| return -1; | |
| } | |
| return MEDIA_PLAYLIST; | |
| } | |
| /* | |
| * In response to | |
| * http://stackoverflow.com/questions/1634359/is-there-a-reverse-fn-for-strstr | |
| * | |
| * Basically, strstr but return last occurence, not first. | |
| * | |
| * This file contains several implementations and a harness to test and | |
| * benchmark them. | |
| * | |
| * Some of the implementations of the actual function are copied from | |
| * elsewhere; they are commented with the location. The rest of the coe | |
| * was written by Lars Wirzenius (liw@liw.fi) and is hereby released into | |
| * the public domain. No warranty. If it turns out to be broken, you get | |
| * to keep the pieces. | |
| */ | |
| /* By liw. */ | |
| static char* last_strstr(const char* haystack, const char* needle) | |
| { | |
| if (*needle == '\0') | |
| return (char*)haystack; | |
| char* result = NULL; | |
| for (;;) { | |
| char* p = strstr(haystack, needle); | |
| if (p == NULL) | |
| break; | |
| result = p; | |
| haystack = p + 1; | |
| } | |
| return result; | |
| } | |
| static int extend_url(char **url, const char *baseurl) | |
| { | |
| static const char proxy_marker[] = "englandproxy.co.uk"; // ugly workaround to be fixed | |
| static const char proxy_url[] = "http://www.englandproxy.co.uk/"; | |
| size_t max_length = strlen(*url) + strlen(baseurl) + 10; | |
| if (!strncmp(*url, "http://", 7) || !strncmp(*url, "https://", 8)) { | |
| if (strstr(baseurl, proxy_marker) && !strstr(*url, proxy_marker)) { | |
| max_length = strlen(*url) + strlen(proxy_url); | |
| char *buffer = malloc(max_length); | |
| snprintf(buffer, max_length, "%s%s", proxy_url, strstr(*url, "://") + 3); | |
| *url = realloc(*url, strlen(buffer) + 1); | |
| strcpy(*url, buffer); | |
| free(buffer); | |
| } | |
| return 0; | |
| } | |
| else if (**url == '/') { | |
| char *domain = malloc(max_length); | |
| strcpy(domain, baseurl); | |
| char proto[6]; | |
| if( 2 == sscanf(baseurl, "%5[^:]://%[^/]", proto, domain)) | |
| { | |
| char *buffer = malloc(max_length); | |
| if ( (*url)[1] == '/') // url start with "//" | |
| { | |
| snprintf(buffer, max_length, "%s:%s", proto, *url); | |
| } | |
| else | |
| { | |
| snprintf(buffer, max_length, "%s://%s%s", proto, domain, *url); | |
| } | |
| *url = realloc(*url, strlen(buffer) + 1); | |
| strcpy(*url, buffer); | |
| free(buffer); | |
| } | |
| free(domain); | |
| return 0; | |
| } | |
| else if (strstr(baseurl, "://")) { | |
| // URLs can have '?'. To make /../ work, remove it. | |
| char *domain = strdup(baseurl); | |
| char *find_questionmark = strchr(domain, '?'); | |
| if (find_questionmark) { | |
| *find_questionmark = '\0'; | |
| } | |
| char *buffer = malloc(max_length); | |
| snprintf(buffer, max_length, "%s/../%s", domain, *url); | |
| *url = realloc(*url, strlen(buffer) + 1); | |
| strcpy(*url, buffer); | |
| free(buffer); | |
| free(domain); | |
| return 0; | |
| } | |
| else { | |
| // Assume local URL | |
| char* folder = strdup(baseurl); | |
| char* separator = "/"; | |
| char* find_folder = last_strstr(folder, separator); | |
| if (find_folder) { | |
| *find_folder = '\0'; | |
| } else { | |
| separator = "\\"; | |
| find_folder = last_strstr(folder, separator); | |
| if (find_folder) | |
| *find_folder = '\0'; | |
| } | |
| if (find_folder) { | |
| char* buffer = malloc(max_length); | |
| snprintf(buffer, max_length, "%s%s%s", folder, separator, *url); | |
| *url = realloc(*url, strlen(buffer) + 1); | |
| strcpy(*url, buffer); | |
| free(buffer); | |
| } | |
| free(folder); | |
| return 0; | |
| } | |
| } | |
| static int parse_tag(hls_media_playlist_t *me, struct hls_media_segment *ms, char *tag, int64_t *seg_offset, int64_t *seg_size) | |
| { | |
| int enc_type; | |
| if (!strncmp(tag, "#EXT-X-KEY:METHOD=AES-128", 25)) { | |
| enc_type = ENC_AES128; | |
| me->enc_aes.iv_is_static = false; | |
| } else if (!strncmp(tag, "#EXT-X-KEY:METHOD=SAMPLE-AES", 28)) { | |
| enc_type = ENC_AES_SAMPLE; | |
| me->enc_aes.iv_is_static = is_playlist_FPS(me->source); | |
| } else { | |
| if (!strncmp(tag, "#EXTINF:", 8)){ | |
| ms->duration_ms = get_duration_ms(tag+8); | |
| return 0; | |
| } else if (!strncmp(tag, "#EXT-X-ENDLIST", 14)){ | |
| me->is_endlist = true; | |
| return 0; | |
| } else if (!strncmp(tag, "#EXT-X-MEDIA-SEQUENCE:", 22)){ | |
| if(sscanf(tag+22, "%d", &(me->first_media_sequence)) == 1){ | |
| return 0; | |
| } | |
| } else if (!strncmp(tag, "#EXT-X-TARGETDURATION:", 22)){ | |
| me->target_duration_ms = get_duration_ms(tag+22); | |
| return 0; | |
| } else if (!strncmp(tag, "#EXT-X-BYTERANGE:", 17)) { | |
| *seg_size = strtoll(tag+17, NULL, 10); | |
| tag = strchr(tag+17, '@'); | |
| if (tag) { | |
| *seg_offset = strtoll(tag+1, NULL, 10); | |
| } | |
| return 0; | |
| } | |
| return 1; | |
| } | |
| me->encryption = true; | |
| me->encryptiontype = enc_type; | |
| char *link_to_key = malloc(strlen(tag) + strlen(me->url) + 10); | |
| char iv_str[STRLEN_BTS(KEYLEN)] = "\0"; | |
| char sep = '\0'; | |
| if ((sscanf(tag, "#EXT-X-KEY:METHOD=AES-128,URI=\"%[^\"]\",IV=0%c%32[0-9a-f]", link_to_key, &sep, iv_str) > 0 || | |
| sscanf(tag, "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=\"%[^\"]\",IV=0%c%32[0-9a-f]", link_to_key, &sep, iv_str) > 0)) | |
| { | |
| if (sep == 'x' || sep == 'X') | |
| { | |
| uint8_t *iv_bin = malloc(KEYLEN); | |
| str_to_bin(iv_bin, iv_str, KEYLEN); | |
| memcpy(me->enc_aes.iv_value, iv_bin, KEYLEN); | |
| me->enc_aes.iv_is_static = true; | |
| free(iv_bin); | |
| } | |
| extend_url(&link_to_key, me->url); | |
| free(me->enc_aes.key_url); | |
| me->enc_aes.key_url = strdup(link_to_key); | |
| } | |
| free(link_to_key); | |
| return 0; | |
| } | |
| static int media_playlist_get_links(hls_media_playlist_t *me) | |
| { | |
| struct hls_media_segment *ms = NULL; | |
| struct hls_media_segment *curr_ms = NULL; | |
| char *src = me->source; | |
| int64_t seg_offset = 0; | |
| int64_t seg_size = -1; | |
| MSG_PRINT("> START media_playlist_get_links\n"); | |
| int i = 0; | |
| while(src != NULL){ | |
| if (ms == NULL) | |
| { | |
| ms = malloc(sizeof(struct hls_media_segment)); | |
| memset(ms, 0x00, sizeof(struct hls_media_segment)); | |
| } | |
| while ((src = (strchr(src, '\n')))) { | |
| src++; | |
| if (*src == '\n') { | |
| continue; | |
| } | |
| if (*src == '\r') { | |
| continue; | |
| } | |
| if (*src == '#') { | |
| parse_tag(me, ms, src, &seg_offset, &seg_size); | |
| continue; | |
| } | |
| if (*src == '\0') { | |
| goto finish; | |
| } | |
| char *end_ptr = strchr(src, '\n'); | |
| char *end_ptr2 = strchr(src, '\r'); | |
| if (end_ptr2 && end_ptr2 < end_ptr) | |
| end_ptr = end_ptr2; | |
| if (end_ptr != NULL) { | |
| int url_size = (int)(end_ptr - src) + 1; | |
| ms->url = malloc(url_size); | |
| strncpy(ms->url, src, url_size-1); | |
| ms->url[url_size-1] = '\0'; | |
| ms->sequence_number = i + me->first_media_sequence; | |
| if (me->encryptiontype == ENC_AES128 || me->encryptiontype == ENC_AES_SAMPLE) { | |
| memcpy(ms->enc_aes.key_value, me->enc_aes.key_value, KEYLEN); | |
| memcpy(ms->enc_aes.iv_value, me->enc_aes.iv_value, KEYLEN); | |
| ms->enc_aes.key_url = strdup(me->enc_aes.key_url); | |
| if (me->enc_aes.iv_is_static == false) { | |
| char iv_str[STRLEN_BTS(KEYLEN)]; | |
| snprintf(iv_str, STRLEN_BTS(KEYLEN), "%032x\n", ms->sequence_number); | |
| uint8_t *iv_bin = malloc(KEYLEN); | |
| str_to_bin(iv_bin, iv_str, KEYLEN); | |
| memcpy(ms->enc_aes.iv_value, iv_bin, KEYLEN); | |
| free(iv_bin); | |
| } | |
| } | |
| /* Get full url */ | |
| extend_url(&(ms->url), me->url); | |
| ms->size = seg_size; | |
| if (seg_size >= 0) { | |
| ms->offset = seg_offset; | |
| seg_offset += seg_size; | |
| seg_size = -1; | |
| } else { | |
| ms->offset = 0; | |
| seg_offset = 0; | |
| } | |
| /* Add new segment to segment list */ | |
| if (me->first_media_segment == NULL) | |
| { | |
| me->first_media_segment = ms; | |
| curr_ms = ms; | |
| } | |
| else | |
| { | |
| curr_ms->next = ms; | |
| ms->prev = curr_ms; | |
| curr_ms = ms; | |
| } | |
| ms = NULL; | |
| i += 1; | |
| break; | |
| } | |
| } | |
| } | |
| finish: | |
| me->last_media_segment = curr_ms; | |
| if (i > 0) { | |
| me->last_media_sequence = me->first_media_sequence + i - 1; | |
| } | |
| media_segment_cleanup(ms); | |
| MSG_PRINT("> END media_playlist_get_links\n"); | |
| return 0; | |
| } | |
| static uint64_t get_duration_hls_media_playlist(hls_media_playlist_t *me) | |
| { | |
| uint64_t duration_ms = 0; | |
| struct hls_media_segment *ms = me->first_media_segment; | |
| while(ms) { | |
| duration_ms += ms->duration_ms; | |
| ms = ms->next; | |
| } | |
| return duration_ms; | |
| } | |
| int handle_hls_media_playlist(hls_media_playlist_t *me) | |
| { | |
| me->encryption = false; | |
| me->encryptiontype = ENC_NONE; | |
| if (!me->source) { | |
| size_t size = 0; | |
| long http_code = 0; | |
| int tries = hls_args.open_max_retries; | |
| while (tries) { | |
| http_code = get_hls_data_from_url(me->orig_url, &me->source, &size, STRING, &me->url); | |
| if (200 != http_code || size == 0) { | |
| MSG_ERROR("%s %d tries[%d]\n", me->orig_url, (int)http_code, (int)tries); | |
| --tries; | |
| sleep(1); | |
| continue; | |
| } | |
| break; | |
| } | |
| } | |
| me->first_media_segment = NULL; | |
| me->last_media_segment = NULL; | |
| me->target_duration_ms = 0; | |
| me->is_endlist = false; | |
| me->last_media_sequence = 0; | |
| if (media_playlist_get_links(me)) { | |
| MSG_ERROR("Could not parse links. Exiting.\n"); | |
| return 1; | |
| } | |
| me->total_duration_ms = get_duration_hls_media_playlist(me); | |
| return 0; | |
| } | |
| static bool get_next_attrib(char **source, char **tag, char **val) | |
| { | |
| bool ret = false; | |
| char *ptr = NULL; | |
| char *token = NULL; | |
| char *value = NULL; | |
| char end_val_marker = '\0'; | |
| char *src = *source; | |
| while (*src != '\0' && strchr(", \t\n\r", *src)) { | |
| ++src; | |
| continue; | |
| } | |
| ptr = src; | |
| while (*ptr != '=' && *ptr != '\0' && !strchr("\n\r", *ptr)) ++ptr; | |
| if (*ptr != '\0') { | |
| token = src; | |
| *ptr = '\0'; | |
| ptr += 1; | |
| if (*ptr == '"') { | |
| ++ptr; | |
| end_val_marker = '"'; | |
| } else { | |
| end_val_marker = ','; | |
| } | |
| value = ptr; | |
| while (*ptr != end_val_marker && *ptr != '\0' && !strchr("\n\r", *ptr)) ++ptr; | |
| src = ptr; | |
| if (*ptr) { | |
| ++src; | |
| *ptr = '\0'; | |
| } | |
| if (*value) { | |
| *val = value; | |
| *tag = token; | |
| ret = true; | |
| } | |
| *source = src; | |
| } else { | |
| *source = ptr; | |
| } | |
| return ret; | |
| } | |
| int handle_hls_master_playlist(struct hls_master_playlist *ma) | |
| { | |
| bool url_expected = false; | |
| unsigned int bitrate = 0; | |
| char *res = NULL; | |
| char *codecs = NULL; | |
| char *audio_grp = NULL; | |
| char *token = NULL; | |
| char *value = NULL; | |
| char *src = ma->source; | |
| while(*src != '\0'){ | |
| char *end_ptr = strchr(src, '\n'); | |
| if (!end_ptr) { | |
| goto finish; | |
| } | |
| *end_ptr = '\0'; | |
| if (*src == '#') { | |
| url_expected = false; | |
| bitrate = 0; | |
| res = NULL; | |
| codecs = NULL; | |
| audio_grp = NULL; | |
| if (!strncmp(src, "#EXT-X-STREAM-INF:", 18)) { | |
| src += 18; | |
| while (get_next_attrib(&src, &token, &value)) { | |
| if (!strncmp(token, "BANDWIDTH", 9)) { | |
| sscanf(value, "%u", &bitrate); | |
| } else if (!strncmp(token, "AUDIO", 5)) { | |
| audio_grp = value; | |
| } else if (!strncmp(token, "RESOLUTION", 10)) { | |
| res = value; | |
| } else if (!strncmp(token, "CODECS", 6)) { | |
| codecs = value; | |
| } | |
| } | |
| url_expected = true; | |
| } else if (!strncmp(src, "#EXT-X-MEDIA:TYPE=AUDIO,", 24)) { | |
| src += 24; | |
| char *grp_id = NULL; | |
| char *name = NULL; | |
| char *lang = NULL; | |
| char *url = NULL; | |
| bool is_default = false; | |
| while (get_next_attrib(&src, &token, &value)) { | |
| if (!strncmp(token, "GROUP-ID", 8)) { | |
| grp_id = value; | |
| } else if (!strncmp(token, "NAME", 4)) { | |
| name = value; | |
| } else if (!strncmp(token, "LANGUAGE", 8)) { | |
| lang = value; | |
| } else if (!strncmp(token, "URI", 3)) { | |
| url = value; | |
| } else if (!strncmp(token, "DEFAULT", 7)) { | |
| if (!strncmp(value, "YES", 3)) { | |
| is_default = true; | |
| } | |
| } | |
| } | |
| if (grp_id && name && url) { | |
| size_t len = strlen(url); | |
| hls_audio_t *audio = malloc(sizeof(hls_audio_t)); | |
| memset(audio, 0x00, sizeof(hls_audio_t)); | |
| audio->url = malloc(len + 1); | |
| memcpy(audio->url, url, len); | |
| audio->url[len] = '\0'; | |
| extend_url(&(audio->url), ma->url); | |
| audio->grp_id = strdup(grp_id); | |
| audio->name = strdup(name); | |
| audio->lang = lang ? strdup(lang) : NULL; | |
| audio->is_default = is_default; | |
| if (ma->audio) { | |
| audio->next = ma->audio; | |
| } | |
| ma->audio = audio; | |
| } | |
| } | |
| } else if (url_expected) { | |
| size_t len = strlen(src); | |
| // here we will fill new playlist | |
| hls_media_playlist_t *me = malloc(sizeof(hls_media_playlist_t)); | |
| memset(me, 0x00, sizeof(hls_media_playlist_t)); | |
| me->url = malloc(len + 1); | |
| memcpy(me->url, src, len); | |
| me->url[len] = '\0'; | |
| extend_url(&(me->url), ma->url); | |
| me->bitrate = bitrate; | |
| me->audio_grp = audio_grp ? strdup(audio_grp) : NULL; | |
| me->resolution = res ? strdup(res) : strdup("unknown"); | |
| me->codecs = codecs ? strdup(codecs) : strdup("unknown"); | |
| if (ma->media_playlist) { | |
| me->next = ma->media_playlist; | |
| } | |
| ma->media_playlist = me; | |
| url_expected = false; | |
| } | |
| src = end_ptr + 1; | |
| } | |
| finish: | |
| return 0; | |
| } | |
| static int sample_aes_append_av_data(ByteBuffer_t *out, ByteBuffer_t *in, const uint8_t *pcr, uint16_t pid, uint8_t *cont_count) | |
| { | |
| uint8_t *av_data = in->data; | |
| uint32_t av_size = in->pos; | |
| uint8_t ts_header[4] = {TS_SYNC_BYTE, 0x40, 0x00, 0x10}; | |
| ts_header[1] = ((pid >> 8) & 0x1F) | 0x40; // 0x40 - set payload_unit_start_indicator | |
| ts_header[2] = pid & 0xFF; | |
| uint8_t adapt_header[8] = {0x00}; | |
| uint8_t adapt_header_size = 0; | |
| uint32_t payload_size = TS_PACKET_LENGTH - sizeof(ts_header); | |
| if (pcr[0] & 0x10) { | |
| adapt_header_size = 8; | |
| adapt_header[1] = pcr[0] & 0xF0; // set previus flags: discontinuity_indicator, random_access_indicator, elementary_stream_priority_indicator, PCR_flag | |
| memcpy(adapt_header + 2, pcr + 1, 6); | |
| } else if (pcr[0] & 0x20) { | |
| adapt_header_size = 2; | |
| adapt_header[1] = pcr[0] & 0xF0; // restore flags as described above | |
| } else if (av_size < payload_size) { | |
| adapt_header_size = payload_size - av_size == 1 ? 1 : 2; | |
| } | |
| payload_size -= adapt_header_size; | |
| if (adapt_header_size) { | |
| adapt_header[0] = adapt_header_size - 1; // size without field size | |
| if (av_size < payload_size) { | |
| adapt_header[0] += payload_size - av_size; | |
| } | |
| if (adapt_header[0]) { | |
| ts_header[3] = (ts_header[3] & 0xcf) | 0x30; // set addaptation filed flag | |
| } | |
| } | |
| // ts header | |
| ts_header[3] = (ts_header[3] & 0xf0) | (*cont_count); | |
| *cont_count = (*cont_count + 1) % 16; | |
| memcpy(&out->data[out->pos], ts_header, sizeof(ts_header)); | |
| out->pos += sizeof(ts_header); | |
| // adaptation field | |
| if (adapt_header_size) { | |
| memcpy(&out->data[out->pos], adapt_header, adapt_header_size); | |
| out->pos += adapt_header_size; | |
| } | |
| if (av_size < payload_size) { | |
| uint32_t s; | |
| for(s=0; s < payload_size - av_size; ++s) { | |
| out->data[out->pos + s] = 0xff; | |
| } | |
| out->pos += payload_size - av_size; | |
| payload_size = av_size; | |
| } | |
| memcpy(&out->data[out->pos], av_data, payload_size); | |
| out->pos += payload_size; | |
| av_data += payload_size; | |
| av_size -= payload_size; | |
| if (av_size > 0) { | |
| uint32_t packets_num = av_size / (TS_PACKET_LENGTH - 4); | |
| uint32_t p; | |
| ts_header[1] &= 0xBF; // unset payload_unit_start_indicator | |
| ts_header[3] = (ts_header[3] & 0xcf) | 0x10; // unset addaptation filed flag | |
| for (p=0; p < packets_num; ++p) { | |
| ts_header[3] = (ts_header[3] & 0xf0) | (*cont_count); | |
| *cont_count = (*cont_count + 1) % 16; | |
| memcpy(&out->data[out->pos], ts_header, sizeof(ts_header)); | |
| memcpy(&out->data[out->pos+4], av_data, TS_PACKET_LENGTH - sizeof(ts_header)); | |
| out->pos += TS_PACKET_LENGTH; | |
| av_data += TS_PACKET_LENGTH - sizeof(ts_header); | |
| av_size -= TS_PACKET_LENGTH - sizeof(ts_header); | |
| } | |
| ts_header[3] = (ts_header[3] & 0xcf) | 0x30; // set addaptation filed flag to add aligment | |
| adapt_header[1] = 0; // none flags set, only for alignment | |
| if (av_size > 0) { | |
| ts_header[3] = (ts_header[3] & 0xf0) | (*cont_count); | |
| *cont_count = (*cont_count + 1) % 16; | |
| // add ts_header | |
| memcpy(&out->data[out->pos], ts_header, sizeof(ts_header)); | |
| // add adapt header | |
| adapt_header_size = TS_PACKET_LENGTH - 4 - av_size == 1 ? 1 : 2; | |
| adapt_header[0] = adapt_header_size - 1; // size without field size | |
| if (adapt_header[0]) { | |
| adapt_header[0] += TS_PACKET_LENGTH - 4 - 2 - av_size; | |
| } | |
| memcpy(&out->data[out->pos+4], adapt_header, adapt_header_size); | |
| out->pos += 4 + adapt_header_size; | |
| // add alignment | |
| if (adapt_header[0]) { | |
| int32_t s; | |
| for(s=0; s < adapt_header[0] - 1; ++s) { | |
| out->data[out->pos + s] = 0xff; | |
| } | |
| out->pos += adapt_header[0] -1; | |
| } | |
| // add payload | |
| memcpy(out->data + out->pos, av_data, av_size); | |
| out->pos += av_size; | |
| av_data += av_size; | |
| av_size -= av_size; | |
| } | |
| } | |
| return 0; | |
| } | |
| static uint8_t * remove_emulation_prev(const uint8_t *src, | |
| const uint8_t *src_end, | |
| uint8_t *dst, | |
| uint8_t *dst_end) | |
| { | |
| while (src + 2 < src_end) | |
| if (!*src && !*(src + 1) && *(src + 2) == 3) { | |
| *dst++ = *src++; | |
| *dst++ = *src++; | |
| src++; // remove emulation_prevention_three_byte | |
| } else | |
| *dst++ = *src++; | |
| while (src < src_end) | |
| *dst++ = *src++; | |
| return dst; | |
| } | |
| static int insert_emulation_prev( const uint8_t *src, | |
| const uint8_t *src_end, | |
| uint8_t *dst, | |
| uint8_t *dst_end) | |
| { | |
| int bytes_inserted = 0; | |
| while (src + 2 < src_end && dst + 3 < dst_end) | |
| if (!*src && !*(src + 1) && *(src + 2) < 3) { | |
| *dst++ = *src++; | |
| *dst++ = *src++; | |
| *dst++ = 3; // insert emulation_prevention_three_byte | |
| *dst++ = *src++; | |
| bytes_inserted++; | |
| } | |
| else { | |
| *dst++ = *src++; | |
| } | |
| while (src < src_end) | |
| *dst++ = *src++; | |
| return bytes_inserted; | |
| } | |
| static uint8_t *ff_avc_find_startcode_internal(uint8_t *p, uint8_t *end) | |
| { | |
| uint8_t *a = p + 4 - ((intptr_t)p & 3); | |
| for (end -= 3; p < a && p < end; p++) { | |
| if (p[0] == 0 && p[1] == 0 && p[2] == 1) | |
| return p; | |
| } | |
| for (end -= 3; p < end; p += 4) { | |
| uint32_t x = *(uint32_t*)p; | |
| if ((x - 0x01010101) & (~x) & 0x80808080) { // generic | |
| if (p[1] == 0) { | |
| if (p[0] == 0 && p[2] == 1) | |
| return p; | |
| if (p[2] == 0 && p[3] == 1) | |
| return p+1; | |
| } | |
| if (p[3] == 0) { | |
| if (p[2] == 0 && p[4] == 1) | |
| return p+2; | |
| if (p[4] == 0 && p[5] == 1) | |
| return p+3; | |
| } | |
| } | |
| } | |
| for (end += 3; p < end; p++) { | |
| if (p[0] == 0 && p[1] == 0 && p[2] == 1) | |
| return p; | |
| } | |
| return end + 3; | |
| } | |
| static uint8_t *ff_avc_find_startcode(uint8_t *p, uint8_t *end){ | |
| uint8_t *out= ff_avc_find_startcode_internal(p, end); | |
| if(p<out && out<end && !out[-1]) out--; | |
| return out; | |
| } | |
| static int sample_aes_decrypt_nal_units(hls_media_segment_t *s, uint8_t *buf_in, int size) | |
| { | |
| uint8_t *end = buf_in + size; | |
| uint8_t *nal_start; | |
| uint8_t *nal_end; | |
| uint8_t* nal_new_start = NULL; | |
| int nal_new_allocated = 0; | |
| end = remove_emulation_prev(buf_in, end, buf_in, end); | |
| nal_start = ff_avc_find_startcode(buf_in, end); | |
| for (;;) { | |
| while (nal_start < end && !*(nal_start++)); | |
| if (nal_start == end) | |
| break; | |
| nal_end = ff_avc_find_startcode(nal_start, end); | |
| int nal_unit_type = *nal_start & 0x1F; | |
| int nal_size = nal_end - nal_start; | |
| // NAL unit with length of 48 bytes or fewer is completely unencrypted. | |
| if ((nal_unit_type == 1 || nal_unit_type == 5) && nal_size > 48) { | |
| uint8_t* nal_start_bkup = nal_start; | |
| nal_start += 32; | |
| void *ctx = AES128_CBC_CTX_new(); | |
| AES128_CBC_DecryptInit(ctx, s->enc_aes.key_value, s->enc_aes.iv_value, false); | |
| while (nal_start + 16 < nal_end) { | |
| AES128_CBC_DecryptUpdate(ctx, nal_start, nal_start, 16); | |
| nal_start += 16 * 10; // Each 16-byte block of encrypted data is followed by up to nine 16-byte blocks of unencrypted data. | |
| } | |
| AES128_CBC_free(ctx); | |
| nal_start = nal_start_bkup; | |
| } | |
| int bytes_inserted = 0; | |
| if (nal_size) { | |
| int nal_new_maxsize = nal_size * 4 / 3; | |
| nal_new_start = realloc(nal_new_start, nal_new_maxsize); | |
| nal_new_allocated = MAX(nal_new_allocated, nal_new_maxsize); | |
| bytes_inserted = insert_emulation_prev(nal_start, nal_end, nal_new_start, nal_new_start + nal_new_maxsize); | |
| } | |
| if (bytes_inserted) { | |
| memmove(nal_end + bytes_inserted, nal_end, end - nal_end); | |
| memcpy(nal_start, nal_new_start, nal_size + bytes_inserted); | |
| nal_start = nal_end + bytes_inserted; | |
| end += bytes_inserted; | |
| } | |
| else | |
| nal_start = nal_end; | |
| } | |
| free(nal_new_start); | |
| return (int)(end - buf_in); | |
| } | |
| static int sample_aes_decrypt_audio_data(hls_media_segment_t *s, uint8_t *ptr, uint32_t size, audiotype_t audio_codec) | |
| { | |
| bool (* get_next_frame)(const uint8_t **, const uint8_t *, uint32_t *); | |
| switch (audio_codec) | |
| { | |
| case AUDIO_ADTS: | |
| get_next_frame = adts_get_next_frame; | |
| break; | |
| case AUDIO_AC3: | |
| get_next_frame = ac3_get_next_frame; | |
| break; | |
| case AUDIO_EC3: | |
| get_next_frame = ec3_get_next_frame; | |
| break; | |
| case AUDIO_UNKNOWN: | |
| default: | |
| MSG_ERROR("Wrong audio_codec! Should never happen here > EXIT!\n"); | |
| exit(1); | |
| } | |
| uint8_t *audio_frame = ptr; | |
| uint8_t *end_ptr = ptr + size; | |
| uint32_t frame_length = 0; | |
| while (get_next_frame((const uint8_t **)&audio_frame, end_ptr, &frame_length)) { | |
| // The IV must be reset at the beginning of every packet. | |
| uint8_t leaderSize = 0; | |
| if (audio_codec == AUDIO_ADTS) { | |
| // ADTS headers can contain CRC checks. | |
| // If the CRC check bit is 0, CRC exists. | |
| // | |
| // Header (7 or 9 byte) + unencrypted leader (16 bytes) | |
| leaderSize = (audio_frame[1] & 0x01) ? 23 : 25; | |
| } else { // AUDIO_AC3, AUDIO_EC3 | |
| // AC3 Audio is untested. Sample streams welcome. | |
| // | |
| // unencrypted leader | |
| leaderSize = 16; | |
| } | |
| int tmp_size = frame_length > leaderSize ? (frame_length - leaderSize) & 0xFFFFFFF0 : 0; | |
| if (tmp_size) { | |
| void *ctx = AES128_CBC_CTX_new(); | |
| AES128_CBC_DecryptInit(ctx, s->enc_aes.key_value, s->enc_aes.iv_value, false); | |
| AES128_CBC_DecryptUpdate(ctx, audio_frame + leaderSize, audio_frame + leaderSize, tmp_size); | |
| AES128_CBC_free(ctx); | |
| } | |
| audio_frame += frame_length; | |
| } | |
| return 0; | |
| } | |
| static int sample_aes_handle_pes_data(hls_media_segment_t *s, ByteBuffer_t *out, ByteBuffer_t *in, uint8_t *pcr, uint16_t pid, audiotype_t audio_codec, uint8_t *counter) | |
| { | |
| uint16_t pes_header_size = 0; | |
| // we need to skip PES header it is not part of NAL unit | |
| if (in->pos <= PES_HEADER_SIZE || in->data[0] != 0x00 || in->data[1] != 0x00 || in->data[1] == 0x01) { | |
| MSG_ERROR("Wrong or missing PES header!\n"); | |
| return -1; | |
| } | |
| pes_header_size = in->data[8] + 9; | |
| if (pes_header_size >= in->pos) { | |
| MSG_ERROR("Wrong PES header size %hu!\n", &pes_header_size); | |
| return -1; | |
| } | |
| if (AUDIO_UNKNOWN == audio_codec) { | |
| // handle video data in NAL units | |
| int size = sample_aes_decrypt_nal_units(s, in->data + pes_header_size, in->pos - pes_header_size) + pes_header_size; | |
| // to check if I did not any mistake in offset calculation | |
| if (size > in->pos) { | |
| MSG_ERROR("NAL size after decryption is grater then before - before: %d, after: %d - should never happen!\n", size, in->pos); | |
| exit(-1); | |
| } | |
| // output size could be less then input because the start code emulation prevention could be removed if available | |
| if (size < in->pos) { | |
| // we need to update size in the PES header if it was set | |
| int32_t payload_size = ((uint16_t)(in->data[4]) << 8) | in->data[5]; | |
| if (payload_size > 0) { | |
| payload_size -= in->pos - size; | |
| in->data[4] = (payload_size >> 8) & 0xff; | |
| in->data[5] = payload_size & 0xff; | |
| } | |
| in->pos = size; | |
| } | |
| } else { | |
| sample_aes_decrypt_audio_data(s, in->data + pes_header_size, in->pos - pes_header_size, audio_codec); | |
| } | |
| return sample_aes_append_av_data(out, in, pcr, pid, counter); | |
| } | |
| static int decrypt_sample_aes(hls_media_segment_t *s, ByteBuffer_t *buf) | |
| { | |
| int ret = 0; | |
| fill_key_value(&(s->enc_aes)); | |
| if (buf->len > TS_PACKET_LENGTH && buf->data[0] == TS_SYNC_BYTE) { | |
| pmt_data_t pmt = {0}; | |
| if (find_pmt(buf->data, buf->len, &pmt)) { | |
| bool write_pmt = true; | |
| uint16_t audio_PID = PID_UNSPEC; | |
| uint16_t video_PID = PID_UNSPEC; | |
| audiotype_t audio_codec = AUDIO_UNKNOWN; | |
| uint32_t i; | |
| // https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/HLS_Sample_Encryption/TransportStreamSignaling/TransportStreamSignaling.html | |
| for (i=0; i < pmt.component_num; ++i) { | |
| uint8_t stream_type = pmt.components[i].stream_type; | |
| switch (stream_type) { | |
| case 0xdb: | |
| video_PID = pmt.components[i].elementary_PID; | |
| stream_type = 0x1B; // AVC video stream as defined in ITU-T Rec. H.264 | ISO/IEC 14496-10 Video, or AVC base layer of an HEVC video stream as defined in ITU-T H.265 | ISO/IEC 23008-2 | |
| break; | |
| case 0xcf: | |
| audio_codec = AUDIO_ADTS; | |
| audio_PID = pmt.components[i].elementary_PID; | |
| stream_type = 0x0F; // ISO/IEC 13818-7 Audio with ADTS transport syntax | |
| break; | |
| case 0xc1: | |
| audio_codec = AUDIO_AC3; | |
| audio_PID = pmt.components[i].elementary_PID; | |
| stream_type = 0x81; // User Private / AC-3 (ATSC) | |
| break; | |
| case 0xc2: | |
| audio_codec = AUDIO_EC3; | |
| audio_PID = pmt.components[i].elementary_PID; | |
| stream_type = 0x87; // User Private / E-AC-3 (ATSC) | |
| break; | |
| default: | |
| MSG_DBG("Unknown component type: 0x%02hhx, pid: 0x%03hx\n", pmt.components[i].stream_type, pmt.components[i].elementary_PID); | |
| break; | |
| } | |
| if (stream_type != pmt.components[i].stream_type) { | |
| // we update stream type to reflect unencrypted data | |
| pmt.components[i].stream_type = stream_type; | |
| pmt.data[pmt.components[i].offset] = stream_type; | |
| } | |
| } | |
| if (audio_PID != PID_UNSPEC || video_PID != PID_UNSPEC) { | |
| uint8_t audio_counter = 0; | |
| uint8_t video_counter = 0; | |
| uint8_t audio_pcr[7]; // first byte is adaptation filed flags | |
| uint8_t video_pcr[7]; // - || - | |
| ByteBuffer_t outBuffer = {NULL}; | |
| outBuffer.data = malloc(buf->len * 4 / 3); | |
| outBuffer.len = buf->len; | |
| ByteBuffer_t audioBuffer = {NULL}; | |
| ByteBuffer_t videoBuffer = {NULL}; | |
| if (audio_PID != PID_UNSPEC) { | |
| audioBuffer.data = malloc(buf->len); | |
| audioBuffer.len = buf->len; | |
| } | |
| if (video_PID != PID_UNSPEC) { | |
| videoBuffer.data = malloc(buf->len * 4 / 3); // reserve space for emulation_prevention_three_byte | |
| videoBuffer.len = buf->len; | |
| } | |
| // collect all audio and video data | |
| uint32_t packet_id = 0; | |
| uint8_t *ptr = buf->data; | |
| uint8_t *end = ptr + buf->len; | |
| while (ptr + TS_PACKET_LENGTH <= end) { | |
| if (*ptr != TS_SYNC_BYTE) { | |
| MSG_WARNING("Expected sync byte but got 0x%02hhx!\n", *ptr); | |
| ptr += 1; | |
| continue; | |
| } | |
| ts_packet_t packed = {0}; | |
| parse_ts_packet(ptr, &packed); | |
| if (packed.pid == pmt.pid) { | |
| if (write_pmt) { | |
| write_pmt = false; | |
| pmt_update_crc(&pmt); | |
| memcpy(&outBuffer.data[outBuffer.pos], pmt.data, TS_PACKET_LENGTH); | |
| outBuffer.pos += TS_PACKET_LENGTH; | |
| } | |
| } else if (packed.pid == audio_PID || packed.pid == video_PID) { | |
| ByteBuffer_t *pCurrBuffer = packed.pid == audio_PID ? &audioBuffer : &videoBuffer; | |
| uint8_t *pcr = packed.pid == audio_PID ? audio_pcr : video_pcr; | |
| uint8_t *counter = packed.pid == audio_PID ? &audio_counter : &video_counter; | |
| if (packed.unitstart) { | |
| // consume previous data if any | |
| if (pCurrBuffer->pos) { | |
| sample_aes_handle_pes_data(s, &outBuffer, pCurrBuffer, pcr, packed.pid, packed.pid == audio_PID ? audio_codec : AUDIO_UNKNOWN, counter); | |
| } | |
| if ((packed.afc & 2) && (ptr[5] & 0x10)) { // remember PCR if available | |
| memcpy(pcr, ptr + 4 + 1, 7); | |
| } else if ((packed.afc & 2) && (ptr[5] & 0x20)) { // remember discontinuity_indicator if set | |
| pcr[0] = ptr[5]; | |
| } else { | |
| pcr[0] = 0; | |
| } | |
| pCurrBuffer->pos = 0; | |
| } | |
| if (packed.payload_offset < TS_PACKET_LENGTH) { | |
| memcpy(&(pCurrBuffer->data[pCurrBuffer->pos]), ptr + packed.payload_offset, TS_PACKET_LENGTH - packed.payload_offset); | |
| pCurrBuffer->pos += TS_PACKET_LENGTH - packed.payload_offset; | |
| } | |
| } else { | |
| memcpy(&outBuffer.data[outBuffer.pos], ptr, TS_PACKET_LENGTH); | |
| outBuffer.pos += TS_PACKET_LENGTH; | |
| } | |
| ptr += TS_PACKET_LENGTH; | |
| packet_id += 1; | |
| } | |
| if (audioBuffer.pos) { | |
| sample_aes_handle_pes_data(s, &outBuffer, &audioBuffer, audio_pcr, audio_PID, audio_codec, &audio_counter); | |
| } | |
| if (videoBuffer.pos) { | |
| sample_aes_handle_pes_data(s, &outBuffer, &videoBuffer, video_pcr, video_PID, AUDIO_UNKNOWN, &video_counter); | |
| } | |
| if (outBuffer.pos > buf->len ) { | |
| MSG_ERROR("decrypt_sample_aes - buffer overflow detected!\n"); | |
| exit(-1); | |
| } | |
| free(videoBuffer.data); | |
| free(audioBuffer.data); | |
| // replace encrypted data with decrypted one | |
| free(buf->data); | |
| buf->data = outBuffer.data; | |
| buf->len = outBuffer.pos; | |
| } else { | |
| MSG_WARNING("None audio nor video component found!\n"); | |
| ret = -3; | |
| } | |
| } else { | |
| MSG_WARNING("PMT could not be found!\n"); | |
| ret = -2; | |
| } | |
| } else { | |
| MSG_WARNING("Unknown segment type!\n"); | |
| ret = -1; | |
| } | |
| return ret; | |
| } | |
| static int decrypt_aes128(hls_media_segment_t *s, ByteBuffer_t *buf) | |
| { | |
| // The AES128 method encrypts whole segments. | |
| // Simply decrypting them is enough. | |
| fill_key_value(&(s->enc_aes)); | |
| void *ctx = AES128_CBC_CTX_new(); | |
| /* some AES-128 encrypted segments could be not correctly padded | |
| * and decryption with padding will fail - example stream with such problem is welcome | |
| * From other hand dump correctly padded segment will contain trashes, which will cause many | |
| * errors during processing such TS, for example by DVBInspector, | |
| * if padding will be not removed. | |
| */ | |
| #if 1 | |
| int out_size = 0; | |
| AES128_CBC_DecryptInit(ctx, s->enc_aes.key_value, s->enc_aes.iv_value, true); | |
| AES128_CBC_DecryptPadded(ctx, buf->data, buf->data, buf->len, &out_size); | |
| // decoded data size could be less then input because of the padding | |
| buf->len = out_size; | |
| #else | |
| AES128_CBC_DecryptInit(ctx, s->enc_aes.key_value, s->enc_aes.iv_value, false); | |
| AES128_CBC_DecryptUpdate(ctx, buf->data, buf->data, buf->len); | |
| #endif | |
| AES128_CBC_free(ctx); | |
| return 0; | |
| } | |
| static void *hls_playlist_update_thread(void *arg) | |
| { | |
| #ifndef _MSC_VER | |
| char threadname[50]; | |
| strncpy(threadname, __func__, sizeof(threadname)); | |
| threadname[49] = '\0'; | |
| #if !defined(__APPLE__) && !defined(__MINGW32__) && !defined(__CYGWIN__) | |
| prctl(PR_SET_NAME, (unsigned long)&threadname); | |
| #endif | |
| #endif | |
| hls_playlist_updater_params *updater_params = arg; | |
| hls_media_playlist_t *me = updater_params->media_playlist; | |
| pthread_mutex_t *media_playlist_mtx = (pthread_mutex_t *)(updater_params->media_playlist_mtx); | |
| pthread_cond_t *media_playlist_refresh_cond = (pthread_cond_t *)(updater_params->media_playlist_refresh_cond); | |
| pthread_cond_t *media_playlist_empty_cond = (pthread_cond_t *)(updater_params->media_playlist_empty_cond); | |
| void *session = init_hls_session(); | |
| set_timeout_session(session, 2L, 3L); | |
| bool is_endlist = false; | |
| //char *url = NULL; | |
| int refresh_delay_s = 0; | |
| // no lock is needed here because download_live_hls not change this fields | |
| //pthread_mutex_lock(media_playlist_mtx); | |
| is_endlist = me->is_endlist; | |
| if (hls_args.refresh_delay_sec < 0) { | |
| refresh_delay_s = (int)(me->target_duration_ms / 1000); | |
| //pthread_mutex_unlock(media_playlist_mtx); | |
| if (refresh_delay_s > HLSDL_MAX_REFRESH_DELAY_SEC) { | |
| refresh_delay_s = HLSDL_MAX_REFRESH_DELAY_SEC; | |
| } else if (refresh_delay_s < HLSDL_MIN_REFRESH_DELAY_SEC) { | |
| refresh_delay_s = HLSDL_MIN_REFRESH_DELAY_SEC; | |
| } | |
| } else { | |
| refresh_delay_s = hls_args.refresh_delay_sec; | |
| } | |
| struct timespec ts; | |
| memset(&ts, 0x00, sizeof(ts)); | |
| MSG_VERBOSE("Update thread started\n"); | |
| while (!is_endlist) { | |
| // download live hls can interrupt waiting | |
| ts.tv_sec = time(NULL) + refresh_delay_s; | |
| pthread_mutex_lock(media_playlist_mtx); | |
| pthread_cond_timedwait(media_playlist_refresh_cond, media_playlist_mtx, &ts); | |
| pthread_mutex_unlock(media_playlist_mtx); | |
| // update playlist | |
| hls_media_playlist_t new_me; | |
| memset(&new_me, 0x00, sizeof(new_me)); | |
| size_t size = 0; | |
| MSG_PRINT("> START DOWNLOAD LIST url[%s]\n", me->url); | |
| long http_code = get_data_from_url_with_session(&session, me->url, &new_me.source, &size, STRING, &(new_me.url), -1, -1); | |
| MSG_PRINT("> END DOWNLOAD LIST\n"); | |
| if (200 == http_code && 0 == media_playlist_get_links(&new_me)) { | |
| // no mutex is needed here because download_live_hls not change this fields | |
| if (new_me.is_endlist || | |
| new_me.first_media_sequence != me->first_media_sequence || | |
| new_me.last_media_sequence != me->last_media_sequence) | |
| { | |
| bool list_extended = false; | |
| // we need to update list | |
| pthread_mutex_lock(media_playlist_mtx); | |
| me->is_endlist = new_me.is_endlist; | |
| is_endlist = new_me.is_endlist; | |
| me->first_media_sequence = new_me.first_media_sequence; | |
| if (new_me.last_media_sequence > me->last_media_sequence) | |
| { | |
| // add new segments | |
| struct hls_media_segment *ms = new_me.first_media_segment; | |
| while (ms) { | |
| if (ms->sequence_number > me->last_media_sequence) { | |
| if (ms->prev) { | |
| ms->prev->next = NULL; | |
| } | |
| ms->prev = NULL; | |
| if (me->last_media_segment) { | |
| me->last_media_segment->next = ms; | |
| } else { | |
| assert(me->first_media_segment == NULL); | |
| me->first_media_segment = ms; | |
| } | |
| me->last_media_segment = new_me.last_media_segment; | |
| me->last_media_sequence = new_me.last_media_sequence; | |
| if (ms == new_me.first_media_segment) { | |
| // all segments are new | |
| new_me.first_media_segment = NULL; | |
| new_me.last_media_segment = NULL; | |
| } | |
| while (ms) { | |
| me->total_duration_ms += ms->duration_ms; | |
| ms = ms->next; | |
| } | |
| list_extended = true; | |
| break; | |
| } | |
| ms = ms->next; | |
| } | |
| } | |
| if (list_extended) { | |
| pthread_cond_signal(media_playlist_empty_cond); | |
| } | |
| pthread_mutex_unlock(media_playlist_mtx); | |
| } | |
| } else { | |
| MSG_WARNING("Fail to update playlist \"%s\". http_code[%d].\n", me->url, (int)http_code); | |
| clean_http_session(session); | |
| sleep(1); | |
| session = init_hls_session(); | |
| if (session) { | |
| set_timeout_session(session, 2L, 15L); | |
| set_fresh_connect_http_session(session, 1); | |
| } | |
| } | |
| media_playlist_cleanup(&new_me); | |
| } | |
| clean_http_session(session); | |
| pthread_exit(NULL); | |
| return NULL; | |
| } | |
| int download_live_hls(write_ctx_t *out_ctx, hls_media_playlist_t *me) | |
| { | |
| MSG_API("{\"d_t\":\"live\"}\n"); | |
| hls_playlist_updater_params updater_params; | |
| /* declaration synchronization prymitives */ | |
| pthread_mutex_t media_playlist_mtx; | |
| pthread_mutex_t cookie_file_mtx; | |
| pthread_cond_t media_playlist_refresh_cond; | |
| pthread_cond_t media_playlist_empty_cond; | |
| /* init synchronization prymitives */ | |
| pthread_mutex_init(&media_playlist_mtx, NULL); | |
| pthread_mutex_init(&cookie_file_mtx, NULL); | |
| pthread_cond_init(&media_playlist_refresh_cond, NULL); | |
| pthread_cond_init(&media_playlist_empty_cond, NULL); | |
| memset(&updater_params, 0x00, sizeof(updater_params)); | |
| updater_params.media_playlist = me; | |
| updater_params.media_playlist_mtx = (void *)&media_playlist_mtx; | |
| updater_params.media_playlist_refresh_cond = (void *)&media_playlist_refresh_cond; | |
| updater_params.media_playlist_empty_cond = (void *)&media_playlist_empty_cond; | |
| hls_args.cookie_file_mutex = (void *)&cookie_file_mtx; | |
| // skip first segments | |
| if (me->first_media_segment != me->last_media_segment) { | |
| struct hls_media_segment *ms = me->last_media_segment; | |
| uint64_t duration_ms = 0; | |
| uint64_t duration_offset_ms = hls_args.live_start_offset_sec * 1000; | |
| while (ms) { | |
| duration_ms += ms->duration_ms; | |
| if (duration_ms >= duration_offset_ms) { | |
| break; | |
| } | |
| ms = ms->prev; | |
| } | |
| if (ms && ms != me->first_media_segment){ | |
| // remove segments | |
| while (me->first_media_segment != ms) { | |
| struct hls_media_segment *tmp_ms = me->first_media_segment; | |
| me->first_media_segment = me->first_media_segment->next; | |
| media_segment_cleanup(tmp_ms); | |
| } | |
| ms->prev = NULL; | |
| me->first_media_segment = ms; | |
| } | |
| me->total_duration_ms = get_duration_hls_media_playlist(me); | |
| } | |
| // start update thread | |
| pthread_t thread; | |
| void *ret; | |
| pthread_create(&thread, NULL, hls_playlist_update_thread, &updater_params); | |
| void *session = init_hls_session(); | |
| set_timeout_session(session, 2L, 3L); | |
| uint64_t downloaded_duration_ms = 0; | |
| int64_t download_size = 0; | |
| time_t repTime = 0; | |
| bool download = true; | |
| while(download) { | |
| pthread_mutex_lock(&media_playlist_mtx); | |
| struct hls_media_segment *ms = me->first_media_segment; | |
| if (ms != NULL) { | |
| me->first_media_segment = ms->next; | |
| if (me->first_media_segment) { | |
| me->first_media_segment->prev = NULL; | |
| } | |
| } | |
| else { | |
| me->last_media_segment = NULL; | |
| download = !me->is_endlist; | |
| } | |
| if (ms == NULL) { | |
| if (download) { | |
| pthread_cond_signal(&media_playlist_refresh_cond); | |
| pthread_cond_wait(&media_playlist_empty_cond, &media_playlist_mtx); | |
| } | |
| } | |
| pthread_mutex_unlock(&media_playlist_mtx); | |
| if (ms == NULL) { | |
| continue; | |
| } | |
| MSG_PRINT("Downloading part %d\n", ms->sequence_number); | |
| int retries = 0; | |
| do { | |
| struct ByteBuffer seg; | |
| memset(&seg, 0x00, sizeof(seg)); | |
| size_t size = 0; | |
| long http_code = get_data_from_url_with_session(&session, ms->url, (char **)&(seg.data), &size, BINARY, NULL, ms->offset, ms->size); | |
| seg.len = (int)size; | |
| if (!(http_code == 200 || (http_code == 206 && (ms->size > -1 || hls_args.accept_partial_content)))) { | |
| int first_media_sequence = 0; | |
| if (seg.data) { | |
| free(seg.data); | |
| seg.data = NULL; | |
| } | |
| pthread_mutex_lock(&media_playlist_mtx); | |
| first_media_sequence = me->first_media_sequence; | |
| pthread_mutex_unlock(&media_playlist_mtx); | |
| if(http_code != 403 && http_code != 401 && http_code != 410 && retries <= hls_args.segment_download_retries && ms->sequence_number > first_media_sequence) { | |
| clean_http_session(session); | |
| sleep(1); | |
| session = init_hls_session(); | |
| if (session) { | |
| set_timeout_session(session, 2L, 5L); | |
| set_fresh_connect_http_session(session, 1); | |
| MSG_WARNING("Live retry segment %d download, due to previous error. http_code[%d].\n", ms->sequence_number, (int)http_code); | |
| retries += 1; | |
| continue; | |
| } | |
| } | |
| else | |
| { | |
| MSG_WARNING("Live mode skipping segment %d. http_code[%d].\n", ms->sequence_number, (int)http_code); | |
| break; | |
| } | |
| } | |
| downloaded_duration_ms += ms->duration_ms; | |
| if (hls_args.live_duration_sec > 0 && downloaded_duration_ms > hls_args.live_duration_sec * 1000) { | |
| download = false; | |
| pthread_cancel(thread); | |
| break; | |
| } | |
| if (me->encryption == true && me->encryptiontype == ENC_AES128) { | |
| decrypt_aes128(ms, &seg); | |
| } else if (me->encryption == true && me->encryptiontype == ENC_AES_SAMPLE) { | |
| decrypt_sample_aes(ms, &seg); | |
| } | |
| download_size += out_ctx->write(seg.data, seg.len, out_ctx->opaque); | |
| free(seg.data); | |
| set_fresh_connect_http_session(session, 0); | |
| time_t curRepTime = time(NULL); | |
| if ((curRepTime - repTime) >= 1) { | |
| MSG_API("{\"t_d\":%u,\"d_d\":%u,\"d_s\":%"PRId64"}\n", (uint32_t)(me->total_duration_ms / 1000), (uint32_t)(downloaded_duration_ms / 1000), download_size); | |
| repTime = curRepTime; | |
| } | |
| break; | |
| } while(true); | |
| media_segment_cleanup(ms); | |
| } | |
| pthread_join(thread, &ret); | |
| pthread_mutex_destroy(&media_playlist_mtx); | |
| pthread_cond_destroy(&media_playlist_refresh_cond); | |
| pthread_cond_destroy(&media_playlist_empty_cond); | |
| pthread_mutex_destroy(&cookie_file_mtx); | |
| hls_args.cookie_file_mutex = NULL; | |
| MSG_API("{\"t_d\":%u,\"d_d\":%u,\"d_s\":%"PRId64"}\n", (uint32_t)(me->total_duration_ms / 1000), (uint32_t)(downloaded_duration_ms / 1000), download_size); | |
| if (session) | |
| { | |
| clean_http_session(session); | |
| } | |
| return 0; | |
| } | |
| static int vod_download_segment(void **psession, hls_media_playlist_t *me, struct hls_media_segment *ms, struct ByteBuffer *seg) | |
| { | |
| int retries = 0; | |
| int ret = 0; | |
| while (true) { | |
| MSG_PRINT("Downloading part %d\n", ms->sequence_number); | |
| memset(seg, 0x00, sizeof(*seg)); | |
| size_t size = 0; | |
| long http_code = get_data_from_url_with_session(psession, ms->url, (char **)&(seg->data), &size, BINARY, NULL, ms->offset, ms->size); | |
| seg->len = (int)size; | |
| if (!(http_code == 200 || (http_code == 206 && (ms->size > -1 || hls_args.accept_partial_content)))) { | |
| if (seg->data) { | |
| free(seg->data); | |
| seg->data = NULL; | |
| } | |
| if (http_code != 403 && http_code != 401 && http_code != 410 && retries <= hls_args.segment_download_retries) { | |
| clean_http_session(*psession); | |
| sleep(1); | |
| *psession = init_hls_session(); | |
| set_timeout_session(*psession, 2L, 30L); | |
| if (*psession) { | |
| set_fresh_connect_http_session(*psession, 1); | |
| MSG_WARNING("VOD retry segment %d download, due to previous error. http_code[%d].\n", ms->sequence_number, (int)http_code); | |
| retries += 1; | |
| continue; | |
| } | |
| } | |
| ret = 1; | |
| MSG_API("{\"error_code\":%d, \"error_msg\":\"http\"}\n", (int)http_code); | |
| break; | |
| } | |
| break; | |
| } | |
| if (ret == 0) { | |
| if (me->encryption == true && me->encryptiontype == ENC_AES128) { | |
| decrypt_aes128(ms, seg); | |
| } else if (me->encryption == true && me->encryptiontype == ENC_AES_SAMPLE) { | |
| decrypt_sample_aes(ms, seg); | |
| } | |
| } | |
| /* normally we want to reuse sessions, | |
| * so restore it in case when fresh session | |
| * was requested do to re-try | |
| */ | |
| if (retries) { | |
| set_fresh_connect_http_session(*psession, 0); | |
| } | |
| return ret; | |
| } | |
| bool consecutive_sync_byte(uint8_t *buf, size_t len, uint8_t n) { | |
| if (len < n * TS_PACKET_LENGTH) { | |
| return false; | |
| } | |
| for (uint8_t i = 1; i < n; ++i) { | |
| if (buf[i * TS_PACKET_LENGTH] != TS_SYNC_BYTE) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| uint8_t * find_first_ts_packet(ByteBuffer_t *buf) { | |
| uint8_t *cursor = buf->data; | |
| size_t len = buf->len; | |
| while (TS_PACKET_LENGTH <= len) { | |
| uint8_t *next = memchr(cursor, TS_SYNC_BYTE, len); | |
| if (next == NULL) { | |
| return NULL; | |
| } | |
| len -= (next - cursor); | |
| cursor = next; | |
| if (consecutive_sync_byte(cursor, len, 3)) { | |
| return cursor; | |
| } | |
| ++cursor; | |
| --len; | |
| } | |
| return NULL; | |
| } | |
| int download_hls(write_ctx_t *out_ctx, hls_media_playlist_t *me, hls_media_playlist_t *me_audio) | |
| { | |
| MSG_VERBOSE("Downloading segments.\n"); | |
| MSG_API("{\"d_t\":\"vod\"}\n"); // d_t - download type | |
| MSG_API("{\"t_d\":%u,\"d_d\":0, \"d_s\":0}\n", (uint32_t)(me->total_duration_ms / 1000)); // t_d - total duration, d_d - download duration, d_s - download size | |
| int ret = 0; | |
| void *session = init_hls_session(); | |
| set_timeout_session(session, 2L, 3L); | |
| assert(session); | |
| time_t repTime = 0; | |
| uint64_t downloaded_duration_ms = 0; | |
| int64_t download_size = 0; | |
| struct ByteBuffer seg; | |
| struct ByteBuffer seg_audio; | |
| struct hls_media_segment *ms = me->first_media_segment; | |
| struct hls_media_segment *ms_audio = NULL; | |
| merge_context_t merge_context; | |
| if (me_audio) { | |
| ms_audio = me_audio->first_media_segment; | |
| memset(&merge_context, 0x00, sizeof(merge_context)); | |
| merge_context.out = out_ctx; | |
| } | |
| while(ms) { | |
| if (0 != vod_download_segment(&session, me, ms, &seg)) { | |
| break; | |
| } | |
| uint8_t *first_video_packet = find_first_ts_packet(&seg); | |
| uint8_t *first_audio_packet = NULL; | |
| if (ms_audio) { | |
| if ( 0 != vod_download_segment(&session, me_audio, ms_audio, &seg_audio)) { | |
| break; | |
| } | |
| first_audio_packet = find_first_ts_packet(&seg_audio); | |
| } | |
| // first segment should be TS for success merge | |
| if (first_video_packet && first_audio_packet) { | |
| size_t video_len = seg.len - (first_video_packet - seg.data); | |
| size_t audio_len = seg_audio.len - (first_audio_packet - seg_audio.data); | |
| download_size += merge_packets( | |
| &merge_context, | |
| first_video_packet, | |
| video_len, | |
| first_audio_packet, | |
| audio_len | |
| ); | |
| } else { | |
| download_size += out_ctx->write(seg.data, seg.len, out_ctx->opaque); | |
| } | |
| if (ms_audio) { | |
| free(seg_audio.data); | |
| ms_audio = ms_audio->next; | |
| } | |
| free(seg.data); | |
| downloaded_duration_ms += ms->duration_ms; | |
| time_t curRepTime = time(NULL); | |
| if ((curRepTime - repTime) >= 1) { | |
| MSG_API("{\"t_d\":%u,\"d_d\":%u,\"d_s\":%"PRId64"}\n", (uint32_t)(me->total_duration_ms / 1000), (uint32_t)(downloaded_duration_ms / 1000), download_size); | |
| repTime = curRepTime; | |
| } | |
| ms = ms->next; | |
| } | |
| MSG_API("{\"t_d\":%u,\"d_d\":%u,\"d_s\":%"PRId64"}\n", (uint32_t)(me->total_duration_ms / 1000), (uint32_t)(downloaded_duration_ms / 1000), download_size); | |
| if (session) { | |
| clean_http_session(session); | |
| } | |
| return ret; | |
| } | |
| int print_enc_keys(hls_media_playlist_t *me) | |
| { | |
| struct hls_media_segment *ms = me->first_media_segment; | |
| while(ms) { | |
| if (me->encryption == true) { | |
| fill_key_value(&(ms->enc_aes)); | |
| MSG_PRINT("[AES-128]KEY: 0x"); | |
| for(size_t count = 0; count < KEYLEN; count++) { | |
| MSG_PRINT("%02x", ms->enc_aes.key_value[count]); | |
| } | |
| MSG_PRINT(" IV: 0x"); | |
| for(size_t count = 0; count < KEYLEN; count++) { | |
| MSG_PRINT("%02x", ms->enc_aes.iv_value[count]); | |
| } | |
| MSG_PRINT("\n"); | |
| } | |
| ms = ms->next; | |
| } | |
| return 0; | |
| } | |
| void media_segment_cleanup(struct hls_media_segment *ms) | |
| { | |
| if (ms) | |
| { | |
| free(ms->url); | |
| free(ms->enc_aes.key_url); | |
| free(ms); | |
| } | |
| } | |
| void media_playlist_cleanup(hls_media_playlist_t *me) | |
| { | |
| struct hls_media_segment *ms = me->first_media_segment; | |
| free(me->source); | |
| free(me->orig_url); | |
| free(me->url); | |
| free(me->audio_grp); | |
| free(me->resolution); | |
| free(me->codecs); | |
| free(me->enc_aes.key_url); | |
| while(ms){ | |
| me->first_media_segment = ms->next; | |
| media_segment_cleanup(ms); | |
| ms = me->first_media_segment; | |
| } | |
| assert(me->first_media_segment == NULL); | |
| me->last_media_segment = NULL; | |
| } | |
| static void audio_cleanup(hls_audio_t *audio) | |
| { | |
| free(audio->url); | |
| free(audio->grp_id); | |
| free(audio->lang); | |
| free(audio->name); | |
| } | |
| void master_playlist_cleanup(struct hls_master_playlist *ma) | |
| { | |
| hls_media_playlist_t *me = ma->media_playlist; | |
| while (me) { | |
| hls_media_playlist_t *ptr = me; | |
| me = me->next; | |
| media_playlist_cleanup(ptr); | |
| free(ptr); | |
| } | |
| hls_audio_t *audio = ma->audio; | |
| while (audio) { | |
| hls_audio_t *ptr = audio; | |
| audio = audio->next; | |
| audio_cleanup(ptr); | |
| free(ptr); | |
| } | |
| free(ma->source); | |
| free(ma->orig_url); | |
| free(ma->url); | |
| } | |
| int fill_key_value(struct enc_aes128 *es) | |
| { | |
| /* temporary we will create cache with keys url here | |
| * this will make this function thread unsafe but at now | |
| * it is not problem because it is used only from one thread | |
| * | |
| * last allocation of cache_key_url will not be free | |
| * but this is not big problem since this code is run | |
| * as standalone process | |
| *(system will free all memory allocated by process at it exit). | |
| * | |
| * But this must be fixed for clear valgrind memory leak detection. | |
| */ | |
| static char cache_key_value[KEYLEN] = ""; | |
| static char *cache_key_url = NULL; | |
| if (es && es->key_url) | |
| { | |
| if (cache_key_url && 0 == strcmp(cache_key_url, es->key_url)) | |
| { | |
| memcpy(es->key_value, cache_key_value, KEYLEN); | |
| } | |
| else if (hls_args.key_value) | |
| { | |
| memcpy(es->key_value, hls_args.key_value, KEYLEN); | |
| memcpy(cache_key_value, es->key_value, KEYLEN); | |
| free(cache_key_url); | |
| cache_key_url = strdup(es->key_url); | |
| } else | |
| { | |
| char *key_url = NULL; | |
| char *key_value = NULL; | |
| size_t size = 0; | |
| long http_code = 0; | |
| if (NULL != hls_args.key_uri_replace_old && \ | |
| NULL != hls_args.key_uri_replace_new && \ | |
| '\0' != hls_args.key_uri_replace_old[0]) { | |
| key_url = repl_str(es->key_url, hls_args.key_uri_replace_old, hls_args.key_uri_replace_new); | |
| } else { | |
| key_url = es->key_url; | |
| } | |
| http_code = get_hls_data_from_url(key_url, &key_value, &size, BINKEY, NULL); | |
| if (es->key_url != key_url) { | |
| free(key_url); | |
| } | |
| if (http_code != 200 || size == 0) { | |
| MSG_ERROR("Getting key-file [%s] failed http_code[%d].\n", es->key_url, http_code); | |
| return 1; | |
| } | |
| memcpy(es->key_value, key_value, KEYLEN); | |
| free(key_value); | |
| free(cache_key_url); | |
| cache_key_url = strdup(es->key_url); | |
| memcpy(cache_key_value, es->key_value, KEYLEN); | |
| } | |
| free(es->key_url); | |
| es->key_url = NULL; | |
| } | |
| return 0; | |
| } |