This source file includes following definitions.
- cleanup_list_of_elements
- playlist_element_del
- playlist_element_new
- stream_new
- stream_del
- string2num
- safe_start_equals
- reset_attributes
- extract_attributes
- parse_attributes
- master_playlist_new
- gf_m3u8_master_playlist_del
- master_playlist_find_matching_stream
- gf_m3u8_parse_master_playlist
- declare_sub_playlist
- gf_m3u8_parse_sub_playlist
#define _GNU_SOURCE
#include <gpac/internal/m3u8.h>
#include <gpac/network.h>
typedef struct _s_accumulated_attributes {
char *title;
char *mediaURL;
double duration_in_seconds;
int bandwidth;
int width, height;
int stream_id;
char *codecs;
char *language;
MediaType type;
union {
char *audio;
char *video;
char *subtitle;
char *closed_captions;
} group;
int target_duration_in_seconds;
int min_media_sequence;
int current_media_seq;
int version;
int compatibility_version;
Bool is_master_playlist;
Bool is_media_segment;
Bool is_playlist_ended;
u64 playlist_utc_timestamp;
u64 byte_range_start, byte_range_end;
u64 init_byte_range_start, init_byte_range_end;
PlaylistElementDRMMethod key_method;
char *init_url;
char *key_url;
bin128 key_iv;
Bool independant_segments;
} s_accumulated_attributes;
GF_Err playlist_element_del(PlaylistElement * e);
static GF_Err cleanup_list_of_elements(GF_List *list) {
GF_Err result = GF_OK;
if (list == NULL)
return result;
while (gf_list_count(list)) {
PlaylistElement *pl = (PlaylistElement *) gf_list_get(list, 0);
if (pl)
result |= playlist_element_del(pl);
gf_list_rem(list, 0);
}
gf_list_del(list);
return result;
}
GF_Err playlist_element_del(PlaylistElement * e) {
GF_Err result = GF_OK;
if (e == NULL)
return result;
if (e->title) {
gf_free(e->title);
}
if (e->codecs) {
gf_free(e->codecs);
}
if (e->language) {
gf_free(e->language);
}
if (e->audio_group) {
gf_free(e->audio_group);
}
if (e->video_group) {
gf_free(e->video_group);
}
if (e->key_uri) {
gf_free(e->key_uri);
}
memset(e->key_iv, 0, sizeof(bin128) );
if (e->url)
gf_free(e->url);
switch (e->element_type) {
case TYPE_UNKNOWN:
case TYPE_MEDIA:
break;
case TYPE_PLAYLIST:
assert(e->element.playlist.elements);
result |= cleanup_list_of_elements(e->element.playlist.elements);
default:
break;
}
gf_free(e);
return result;
}
static PlaylistElement* playlist_element_new(PlaylistElementType element_type, const char *url, s_accumulated_attributes *attribs)
{
PlaylistElement *e;
GF_SAFEALLOC(e, PlaylistElement);
if (e == NULL)
return NULL;
e->media_type = attribs->type;
e->duration_info = attribs->duration_in_seconds;
e->byte_range_start = attribs->byte_range_start;
e->byte_range_end = attribs->byte_range_end;
e->title = (attribs->title ? gf_strdup(attribs->title) : NULL);
e->codecs = (attribs->codecs ? gf_strdup(attribs->codecs) : NULL);
e->language = (attribs->language ? gf_strdup(attribs->language) : NULL);
e->drm_method = attribs->key_method;
e->init_segment_url = attribs->init_url ? gf_strdup(attribs->init_url) : NULL;
e->init_byte_range_start = attribs->init_byte_range_start;
e->init_byte_range_end = attribs->init_byte_range_end;
e->key_uri = (attribs->key_url ? gf_strdup(attribs->key_url) : NULL);
memcpy(e->key_iv, attribs->key_iv, sizeof(bin128));
e->utc_start_time = attribs->playlist_utc_timestamp;
assert(url);
e->url = gf_strdup(url);
e->bandwidth = 0;
e->element_type = element_type;
if (element_type == TYPE_PLAYLIST) {
e->element.playlist.is_ended = GF_FALSE;
e->element.playlist.target_duration = attribs->duration_in_seconds;
e->element.playlist.current_media_seq = 0;
e->element.playlist.media_seq_min = 0;
e->element.playlist.media_seq_max = 0;
e->element.playlist.elements = gf_list_new();
if (NULL == (e->element.playlist.elements)) {
if (e->title)
gf_free(e->title);
if (e->codecs)
gf_free(e->codecs);
if (e->language)
gf_free(e->language);
if (e->audio_group)
gf_free(e->audio_group);
if (e->video_group)
gf_free(e->video_group);
if (e->url)
gf_free(e->url);
if (e->init_segment_url)
gf_free(e->init_segment_url);
if (e->key_uri)
gf_free(e->key_uri);
e->url = NULL;
e->title = NULL;
e->codecs = NULL;
e->language = NULL;
e->audio_group = NULL;
e->video_group = NULL;
e->key_uri = NULL;
memset(e->key_iv, 0, sizeof(bin128));
gf_free(e);
return NULL;
}
} else {
}
assert(e->bandwidth == 0);
assert(e->url);
return e;
}
static Stream* stream_new(int stream_id) {
Stream *program = (Stream *) gf_malloc(sizeof(Stream));
if (program == NULL) {
return NULL;
}
program->stream_id = stream_id;
program->variants = gf_list_new();
if (program->variants == NULL) {
gf_free(program);
return NULL;
}
return program;
}
static GF_Err stream_del(Stream *stream) {
GF_Err e = GF_OK;
if (stream == NULL)
return e;
if (stream->variants) {
while (gf_list_count(stream->variants)) {
GF_List *l = gf_list_get(stream->variants, 0);
cleanup_list_of_elements(l);
gf_list_rem(stream->variants, 0);
}
gf_list_del(stream->variants);
}
stream->variants = NULL;
gf_free(stream);
return e;
}
static GFINLINE int string2num(const char *s) {
u64 ret=0, i, shift=2;
u8 hash[GF_SHA1_DIGEST_SIZE];
gf_sha1_csum((u8*)s, (u32)strlen(s), hash);
assert(shift*GF_SHA1_DIGEST_SIZE < 64);
for (i=0; i<GF_SHA1_DIGEST_SIZE; ++i)
ret += (ret << shift) + hash[i];
return (int)(ret % MEDIA_TYPE_AUDIO);
}
#define GROUP_ID_TO_PROGRAM_ID(type, group_id) (\
MEDIA_TYPE_##type + \
string2num(group_id) \
) \
static Bool safe_start_equals(const char *attribute, const char *line) {
size_t len, atlen;
if (line == NULL)
return GF_FALSE;
len = strlen(line);
atlen = strlen(attribute);
if (len < atlen)
return GF_FALSE;
return (0 == strncmp(attribute, line, atlen));
}
static void reset_attributes(s_accumulated_attributes *attributes)
{
memset(attributes, 0, sizeof(s_accumulated_attributes));
attributes->type = MEDIA_TYPE_UNKNOWN;
attributes->min_media_sequence = 1;
attributes->version = 1;
attributes->compatibility_version = 0;
attributes->key_method = DRM_NONE;
}
static char** extract_attributes(const char *name, const char *line, const int num_attributes) {
int sz, i, curr_attribute, start;
char **ret;
u8 quote = 0;
int len = (u32) strlen(line);
start = (u32) strlen(name);
if (len <= start)
return NULL;
if (!safe_start_equals(name, line))
return NULL;
ret = gf_calloc((num_attributes + 1), sizeof(char*));
curr_attribute = 0;
for (i=start; i<=len; i++) {
if (line[i] == '\0' || (!quote && line[i] == ',') || (line[i] == quote)) {
u32 spaces = 0;
sz = i - start;
if (quote && (line[i] == quote))
sz++;
while (line[start+spaces] == ' ')
spaces++;
if ((sz-spaces<=1) && (line[start+spaces]==',')) {
start = i+1;
} else {
if (!strncmp(&line[start+spaces], "\t", sz-spaces) || !strncmp(&line[start+spaces], "\n", sz-spaces)) {
} else {
ret[curr_attribute] = gf_calloc( (1+sz-spaces), sizeof(char));
strncpy(ret[curr_attribute], &(line[start+spaces]), sz-spaces);
curr_attribute++;
}
}
start = i+1;
if (start == len) {
return ret;
}
}
if ((line[i] == '\'') || (line[i] == '"')) {
if (quote) {
quote = 0;
} else {
quote = line[i];
}
}
}
if (curr_attribute == 0) {
gf_free(ret);
return NULL;
}
return ret;
}
#define M3U8_COMPATIBILITY_VERSION(v) \
if (v > attributes->compatibility_version) \
attributes->compatibility_version = v;
static char** parse_attributes(const char *line, s_accumulated_attributes *attributes) {
int int_value, i;
double double_value;
char **ret;
char *end_ptr;
char *utility;
if (line == NULL)
return NULL;
if (!safe_start_equals("#EXT", line))
return NULL;
if (safe_start_equals("#EXT-X-ENDLIST", line)) {
attributes->is_playlist_ended = GF_TRUE;
M3U8_COMPATIBILITY_VERSION(1);
return NULL;
}
attributes->type = MEDIA_TYPE_UNKNOWN;
ret = extract_attributes("#EXT-X-TARGETDURATION:", line, 1);
if (ret) {
if (ret[0]) {
int_value = (s32) strtol(ret[0], &end_ptr, 10);
if (end_ptr != ret[0]) {
attributes->target_duration_in_seconds = int_value;
}
}
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-MEDIA-SEQUENCE:", line, 1);
if (ret) {
if (ret[0]) {
int_value = (s32)strtol(ret[0], &end_ptr, 10);
if (end_ptr != ret[0]) {
attributes->min_media_sequence = int_value;
attributes->current_media_seq = int_value;
}
}
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-VERSION:", line, 1);
if (ret) {
if (ret[0]) {
int_value = (s32)strtol(ret[0], &end_ptr, 10);
if (end_ptr != ret[0]) {
attributes->version = int_value;
}
M3U8_COMPATIBILITY_VERSION(1);
}
return ret;
}
ret = extract_attributes("#EXTINF:", line, 2);
if (ret) {
M3U8_COMPATIBILITY_VERSION(1);
attributes->is_media_segment = GF_TRUE;
if (ret[0]) {
double_value = strtod(ret[0], &end_ptr);
if (end_ptr != ret[0]) {
attributes->duration_in_seconds = double_value;
}
if (strstr(ret[0], ".") || (double_value > (int)double_value)) {
M3U8_COMPATIBILITY_VERSION(3);
}
}
if (ret[1]) {
attributes->title = gf_strdup(ret[1]);
}
return ret;
}
ret = extract_attributes("#EXT-X-KEY:", line, 4);
if (ret) {
const char *method = "METHOD=";
const size_t method_len = strlen(method);
if (safe_start_equals(method, ret[0])) {
if (strncmp(ret[0]+method_len, "NONE", 4)) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] EXT-X-KEY not supported.\n", line));
}
attributes->key_method = DRM_AES_128;
if (ret[1] != NULL && safe_start_equals("URI=\"", ret[1])) {
int_value = (u32) strlen(ret[1]);
if (ret[1][int_value-1] == '"') {
attributes->key_url = gf_strdup(&(ret[1][4]));
}
}
}
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-PROGRAM-DATE-TIME:", line, 1);
if (ret) {
if (ret[0]) attributes->playlist_utc_timestamp = gf_net_parse_date(ret[0]);
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-ALLOW-CACHE:", line, 1);
if (ret) {
GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-ALLOW-CACHE not supported.\n", line));
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-PLAYLIST-TYPE", line, 1);
if (ret) {
if (ret[0] && !strcmp(ret[0], "VOD")) attributes->is_playlist_ended = GF_TRUE;
M3U8_COMPATIBILITY_VERSION(3);
return ret;
}
ret = extract_attributes("#EXT-X-MAP", line, 4);
if (ret) {
i=0;
while (ret[i] != NULL) {
char *val = ret[i];
if (val[0]==':') val++;
if (safe_start_equals("URI=\"", val)) {
char *uri = val + 5;
int_value = (u32) strlen(uri);
if (uri[int_value-1] == '"') {
attributes->init_url = gf_strdup(uri);
attributes->init_url[int_value-1]=0;
}
}
else if (safe_start_equals("BYTERANGE=\"", val)) {
u64 begin, size;
val+=10;
if (sscanf(val, "\""LLU"@"LLU"\"", &size, &begin) == 2) {
if (size) {
attributes->init_byte_range_start = begin;
attributes->init_byte_range_end = begin + size - 1;
} else {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid byte range %s\n", val));
}
}
}
i++;
}
M3U8_COMPATIBILITY_VERSION(3);
return ret;
}
ret = extract_attributes("#EXT-X-STREAM-INF:", line, 10);
if (ret) {
i = 0;
attributes->is_master_playlist = GF_TRUE;
M3U8_COMPATIBILITY_VERSION(1);
while (ret[i] != NULL) {
if (safe_start_equals("BANDWIDTH=", ret[i])) {
utility = &(ret[i][10]);
int_value = (s32) strtol(utility, &end_ptr, 10);
if (end_ptr != utility)
attributes->bandwidth = int_value;
} else if (safe_start_equals("PROGRAM-ID=", ret[i])) {
utility = &(ret[i][11]);
int_value = (s32) strtol(utility, &end_ptr, 10);
if (end_ptr != utility)
attributes->stream_id = int_value;
} else if (safe_start_equals("CODECS=\"", ret[i])) {
int_value = (u32) strlen(ret[i]);
if (ret[i][int_value-1] == '"') {
attributes->codecs = gf_strdup(&(ret[i][7]));
}
} else if (safe_start_equals("RESOLUTION=", ret[i])) {
u32 w, h;
utility = &(ret[i][11]);
if ((sscanf(utility, "%dx%d", &w, &h)==2) || (sscanf(utility, "%dx%d,", &w, &h)==2)) {
attributes->width = w;
attributes->height = h;
}
M3U8_COMPATIBILITY_VERSION(2);
} else if (safe_start_equals("AUDIO=", ret[i])) {
assert(attributes->type == MEDIA_TYPE_UNKNOWN);
attributes->type = MEDIA_TYPE_AUDIO;
attributes->group.audio = gf_strdup(ret[i] + 6);
M3U8_COMPATIBILITY_VERSION(4);
} else if (safe_start_equals("VIDEO=", ret[i])) {
assert(attributes->type == MEDIA_TYPE_UNKNOWN);
attributes->type = MEDIA_TYPE_VIDEO;
attributes->group.video = gf_strdup(ret[i] + 6);
M3U8_COMPATIBILITY_VERSION(4);
}
i++;
}
if (!attributes->bandwidth) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-STREAM-INF: no BANDWIDTH found. Ignoring the line.\n"));
return NULL;
}
return ret;
}
ret = extract_attributes("#EXT-X-DISCONTINUITY", line, 0);
if (ret) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] EXT-X-DISCONTINUITY not supported.\n", line));
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-BYTERANGE:", line, 1);
if (ret) {
if (ret[0]) {
u64 begin, size;
if (sscanf(ret[0], LLU"@"LLU, &size, &begin) == 2) {
if (size) {
attributes->byte_range_start = begin;
attributes->byte_range_end = begin + size - 1;
} else {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid byte range %s\n", ret[0]));
}
}
}
M3U8_COMPATIBILITY_VERSION(4);
return ret;
}
ret = extract_attributes("#EXT-X-MEDIA:", line, 14);
if (ret) {
M3U8_COMPATIBILITY_VERSION(4);
attributes->is_master_playlist = GF_TRUE;
i = 0;
while (ret[i] != NULL) {
if (safe_start_equals("TYPE=", ret[i])) {
if (!strncmp(ret[i]+5, "AUDIO", 5)) {
attributes->type = MEDIA_TYPE_AUDIO;
} else if (!strncmp(ret[i]+5, "VIDEO", 5)) {
attributes->type = MEDIA_TYPE_VIDEO;
} else if (!strncmp(ret[i]+5, "SUBTITLES", 9)) {
attributes->type = MEDIA_TYPE_SUBTITLES;
} else if (!strncmp(ret[i]+5, "CLOSED-CAPTIONS", 15)) {
attributes->type = MEDIA_TYPE_CLOSED_CAPTIONS;
} else {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Unsupported #EXT-X-MEDIA:TYPE=%s\n", ret[i]+5));
}
} else if (safe_start_equals("URI=\"", ret[i])) {
size_t len;
attributes->mediaURL = gf_strdup(ret[i]+5);
len = strlen(attributes->mediaURL);
if (len && (attributes->mediaURL[len-1] == '"')) {
attributes->mediaURL[len-1] = '\0';
} else {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Misformed #EXT-X-MEDIA:URI=%s. Quotes are incorrect.\n", ret[i]+5));
}
} else if (safe_start_equals("GROUP-ID=", ret[i])) {
if (attributes->type == MEDIA_TYPE_AUDIO) {
attributes->group.audio = gf_strdup(ret[i]+9);
attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(AUDIO, attributes->group.audio);
} else if (attributes->type == MEDIA_TYPE_VIDEO) {
attributes->group.video = gf_strdup(ret[i]+9);
attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(VIDEO, attributes->group.video);
} else if (attributes->type == MEDIA_TYPE_SUBTITLES) {
attributes->group.subtitle = gf_strdup(ret[i]+9);
attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(SUBTITLES, attributes->group.subtitle);
} else if (attributes->type == MEDIA_TYPE_CLOSED_CAPTIONS) {
attributes->group.subtitle = gf_strdup(ret[i]+9);
attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(CLOSED_CAPTIONS, attributes->group.subtitle);
} else if (attributes->type == MEDIA_TYPE_UNKNOWN) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:GROUP-ID=%s. Ignoring the line.\n", ret[i]+9));
return NULL;
}
} else if (safe_start_equals("LANGUAGE=\"", ret[i])) {
size_t len;
attributes->language = gf_strdup(ret[i]+9);
len = strlen(attributes->language);
if (len && (attributes->language[len-1] == '"')) {
attributes->language[len-1] = '\0';
} else {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Misformed #EXT-X-MEDIA:LANGUAGE=%s. Quotes are incorrect.\n", ret[i]+5));
}
} else if (safe_start_equals("NAME=", ret[i])) {
GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-MEDIA:NAME not supported\n"));
} else if (safe_start_equals("DEFAULT=", ret[i])) {
GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-MEDIA:DEFAULT not supported\n"));
if (!strncmp(ret[i]+8, "YES", 3)) {
} else if (!strncmp(ret[i]+8, "NO", 2)) {
} else {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:DEFAULT=%s\n", ret[i]+8));
}
} else if (safe_start_equals("AUTOSELECT=", ret[i])) {
GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-MEDIA:AUTOSELECT not supported\n"));
if (!strncmp(ret[i]+11, "YES", 3)) {
} else if (!strncmp(ret[i]+11, "NO", 2)) {
} else {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:AUTOSELECT=%s\n", ret[i]+11));
}
}
i++;
}
if (attributes->type == MEDIA_TYPE_UNKNOWN) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: TYPE is missing. Ignoring the line.\n"));
return NULL;
}
if (attributes->type == MEDIA_TYPE_CLOSED_CAPTIONS && attributes->mediaURL) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: TYPE is CLOSED-CAPTIONS but URI is present. Ignoring the URI.\n"));
gf_free(attributes->mediaURL);
attributes->mediaURL = NULL;
}
if ((attributes->type == MEDIA_TYPE_AUDIO && !attributes->group.audio)
|| (attributes->type == MEDIA_TYPE_VIDEO && !attributes->group.video)) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: missing GROUP-ID attribute. Ignoring the line.\n"));
return NULL;
}
if (!attributes->stream_id) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: no ID was computed. Check previous errors. Ignoring the line.\n"));
return NULL;
}
return ret;
}
if (!strncmp(line, "#EXT-X-INDEPENDENT-SEGMENTS", strlen("#EXT-X-INDEPENDENT-SEGMENTS") )) {
attributes->independant_segments = GF_TRUE;
M3U8_COMPATIBILITY_VERSION(1);
return NULL;
}
if (!strncmp(line, "#EXT-X-I-FRAME-STREAM-INF", strlen("#EXT-X-I-FRAME-STREAM-INF") )) {
return NULL;
}
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Unknown line in M3U8 file %s\n", line));
return NULL;
}
MasterPlaylist* master_playlist_new()
{
MasterPlaylist *pl = (MasterPlaylist*)gf_malloc(sizeof(MasterPlaylist));
if (pl == NULL)
return NULL;
pl->streams = gf_list_new();
if (!pl->streams) {
gf_free(pl);
return NULL;
}
pl->current_stream = -1;
pl->playlist_needs_refresh = GF_TRUE;
return pl;
}
GF_Err gf_m3u8_master_playlist_del(MasterPlaylist **playlist) {
if ((playlist == NULL) || (*playlist == NULL))
return GF_OK;
assert((*playlist)->streams);
while (gf_list_count((*playlist)->streams)) {
Stream *p = gf_list_get((*playlist)->streams, 0);
assert(p);
while (gf_list_count(p->variants)) {
PlaylistElement *pl = gf_list_get(p->variants, 0);
assert(pl);
playlist_element_del(pl);
gf_list_rem(p->variants, 0);
}
gf_list_del(p->variants);
p->variants = NULL;
stream_del(p);
gf_list_rem((*playlist)->streams, 0);
}
gf_list_del((*playlist)->streams);
(*playlist)->streams = NULL;
gf_free(*playlist);
*playlist = NULL;
return GF_OK;
}
static Stream* master_playlist_find_matching_stream(const MasterPlaylist *pl, const u32 stream_id) {
u32 count, i;
assert(pl);
assert(pl->streams);
assert(stream_id >= 0);
count = gf_list_count(pl->streams);
for (i=0; i<count; i++) {
Stream *cur = gf_list_get(pl->streams, i);
assert(cur);
if (stream_id == cur->stream_id) {
return cur;
}
}
return NULL;
}
#define M3U8_BUF_SIZE 2048
GF_EXPORT
GF_Err gf_m3u8_parse_master_playlist(const char *file, MasterPlaylist **playlist, const char *baseURL)
{
return gf_m3u8_parse_sub_playlist(file, playlist, baseURL, NULL, NULL);
}
GF_Err declare_sub_playlist(char *currentLine, const char *baseURL, s_accumulated_attributes *attribs, PlaylistElement *sub_playlist, MasterPlaylist **playlist, Stream *in_stream)
{
u32 i, iv, count;
char *fullURL = currentLine;
if (attribs->is_master_playlist && attribs->is_media_segment) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("[M3U8] Media segment tag MUST NOT appear in a Master Playlist\n"));
return GF_BAD_PARAM;
}
if (gf_url_is_local(fullURL)) {
fullURL = gf_url_concatenate(baseURL, fullURL);
assert(fullURL);
}
GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] declaring %s %s\n", attribs->is_master_playlist ? "sub-playlist" : "media segment", fullURL));
memset(attribs->key_iv, 0, sizeof(bin128) );
iv = gf_htonl(attribs->current_media_seq);
memcpy(attribs->key_iv + 12, (const void *) &iv, sizeof(iv));
{
PlaylistElement *curr_playlist = sub_playlist;
Stream *stream = in_stream;
if (!in_stream)
stream = master_playlist_find_matching_stream(*playlist, attribs->stream_id);
if (stream == NULL) {
stream = stream_new(attribs->stream_id);
if (stream == NULL) {
gf_m3u8_master_playlist_del(playlist);
return GF_OUT_OF_MEM;
}
gf_list_add((*playlist)->streams, stream);
if ((*playlist)->current_stream < 0 && stream->stream_id < MEDIA_TYPE_AUDIO)
(*playlist)->current_stream = stream->stream_id;
}
assert(stream);
assert(stream->variants);
count = gf_list_count(stream->variants);
if (!curr_playlist) {
for (i=0; i<(s32)count; i++) {
PlaylistElement *i_playlist_element = gf_list_get(stream->variants, i);
assert(i_playlist_element);
if (stream->stream_id < MEDIA_TYPE_AUDIO) {
if (attribs->is_media_segment || !strcmp(i_playlist_element->url, fullURL)) {
curr_playlist = i_playlist_element;
break;
}
} else {
}
}
}
if (attribs->is_master_playlist) {
if (curr_playlist != NULL) {
gf_free(fullURL);
return GF_OK;
}
curr_playlist = playlist_element_new(TYPE_PLAYLIST, fullURL, attribs);
if (curr_playlist == NULL) {
gf_m3u8_master_playlist_del(playlist);
return GF_OUT_OF_MEM;
}
assert(fullURL);
if (curr_playlist->url)
gf_free(curr_playlist->url);
curr_playlist->url = gf_strdup(fullURL);
if (curr_playlist->title)
gf_free(curr_playlist->title);
curr_playlist->title = attribs->title ? gf_strdup(attribs->title) : NULL;
if (curr_playlist->codecs)
gf_free(curr_playlist->codecs);
curr_playlist->codecs = attribs->codecs ? gf_strdup(attribs->codecs) : NULL;
if (curr_playlist->audio_group) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[M3U8] Warning: found an AUDIO group in the master playlist."));
}
if (curr_playlist->video_group) {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[M3U8] Warning: found an VIDEO group in the master playlist."));
}
gf_list_add(stream->variants, curr_playlist);
curr_playlist->width = attribs->width;
curr_playlist->height = attribs->height;
} else {
assert((*playlist)->streams);
if (curr_playlist == NULL) {
PlaylistElement *subElement;
assert(baseURL);
curr_playlist = playlist_element_new(TYPE_PLAYLIST, baseURL, attribs);
if (curr_playlist == NULL) {
gf_m3u8_master_playlist_del(playlist);
return GF_OUT_OF_MEM;
}
assert(curr_playlist->element.playlist.elements);
assert(fullURL);
assert(curr_playlist->url && !curr_playlist->codecs);
curr_playlist->codecs = NULL;
subElement = playlist_element_new(TYPE_UNKNOWN, fullURL, attribs);
if (subElement == NULL) {
gf_m3u8_master_playlist_del(playlist);
playlist_element_del(curr_playlist);
return GF_OUT_OF_MEM;
}
gf_list_add(curr_playlist->element.playlist.elements, subElement);
gf_list_add(stream->variants, curr_playlist);
curr_playlist->element.playlist.computed_duration += subElement->duration_info;
assert(stream);
assert(stream->variants);
assert(curr_playlist);
} else {
PlaylistElement *subElement = playlist_element_new(TYPE_UNKNOWN, fullURL, attribs);
if (curr_playlist->element_type != TYPE_PLAYLIST) {
curr_playlist->element_type = TYPE_PLAYLIST;
if (!curr_playlist->element.playlist.elements)
curr_playlist->element.playlist.elements = gf_list_new();
}
if (subElement == NULL) {
gf_m3u8_master_playlist_del(playlist);
playlist_element_del(curr_playlist);
return GF_OUT_OF_MEM;
}
gf_list_add(curr_playlist->element.playlist.elements, subElement);
curr_playlist->element.playlist.computed_duration += subElement->duration_info;
}
}
curr_playlist->element.playlist.current_media_seq = attribs->current_media_seq;
if (attribs->target_duration_in_seconds > 0) {
curr_playlist->element.playlist.target_duration = attribs->target_duration_in_seconds;
curr_playlist->duration_info = attribs->target_duration_in_seconds;
}
if (attribs->duration_in_seconds) {
if (curr_playlist->duration_info == 0) {
curr_playlist->duration_info = attribs->duration_in_seconds;
}
}
curr_playlist->element.playlist.media_seq_min = attribs->min_media_sequence;
curr_playlist->element.playlist.media_seq_max = attribs->current_media_seq;
if (attribs->bandwidth > 1)
curr_playlist->bandwidth = attribs->bandwidth;
if (attribs->is_playlist_ended)
curr_playlist->element.playlist.is_ended = GF_TRUE;
}
if (attribs->title) {
gf_free(attribs->title);
attribs->title = NULL;
}
attribs->duration_in_seconds = 0;
attribs->playlist_utc_timestamp = 0;
attribs->bandwidth = 0;
attribs->stream_id = 0;
if (attribs->codecs != NULL) {
gf_free(attribs->codecs);
attribs->codecs = NULL;
}
if (attribs->language != NULL) {
gf_free(attribs->language);
attribs->language = NULL;
}
if (attribs->group.audio != NULL) {
gf_free(attribs->group.audio);
attribs->group.audio = NULL;
}
if (attribs->group.video != NULL) {
gf_free(attribs->group.video);
attribs->group.video = NULL;
}
if (fullURL != currentLine) {
gf_free(fullURL);
}
return GF_OK;
}
GF_Err gf_m3u8_parse_sub_playlist(const char *file, MasterPlaylist **playlist, const char *baseURL, Stream *in_stream, PlaylistElement *sub_playlist)
{
int len, i, currentLineNumber;
FILE *f = NULL;
char *m3u8_payload;
u32 m3u8_size, m3u8pos;
char currentLine[M3U8_BUF_SIZE];
char **attributes = NULL;
s_accumulated_attributes attribs;
if (!strncmp(file, "gmem://", 7)) {
if (sscanf(file, "gmem://%d@%p", &m3u8_size, &m3u8_payload) != 2) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot Open m3u8 source %s for reading\n", file));
return GF_SERVICE_ERROR;
}
} else {
f = gf_fopen(file, "rt");
if (!f) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot open m3u8 file %s for reading\n", file));
return GF_URL_ERROR;
}
}
if (*playlist == NULL) {
*playlist = master_playlist_new();
if (!(*playlist)) {
if (f) gf_fclose(f);
return GF_OUT_OF_MEM;
}
}
currentLineNumber = 0;
reset_attributes(&attribs);
m3u8pos = 0;
while (1) {
char *eof;
if (f) {
if (!fgets(currentLine, sizeof(currentLine), f))
break;
} else {
u32 __idx = 0;
if (m3u8pos >= m3u8_size)
break;
while (1) {
assert(__idx < M3U8_BUF_SIZE);
currentLine[__idx] = m3u8_payload[m3u8pos];
__idx++;
m3u8pos++;
if ((currentLine[__idx-1]=='\n') || (currentLine[__idx-1]=='\r') || (m3u8pos >= m3u8_size)) {
currentLine[__idx]=0;
break;
}
}
}
currentLineNumber++;
eof = strchr(currentLine, '\r');
if (eof)
eof[0] = '\0';
eof = strchr(currentLine, '\n');
if (eof)
eof[0] = '\0';
len = (u32) strlen(currentLine);
if (len < 1)
continue;
if (currentLineNumber == 1) {
if (len < 7 || (strncmp("#EXTM3U", currentLine, 7) != 0)) {
gf_fclose(f);
gf_m3u8_master_playlist_del(playlist);
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("Failed to parse M3U8 File, it should start with #EXTM3U, but was : %s\n", currentLine));
return GF_STREAM_NOT_FOUND;
}
continue;
}
if (currentLine[0] == '#') {
if (strncmp("#EXT", currentLine, 4) == 0) {
attributes = parse_attributes(currentLine, &attribs);
if (attributes == NULL) {
GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8]Comment at line %d : %s\n", currentLineNumber, currentLine));
} else {
GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] Directive at line %d: \"%s\", attributes=", currentLineNumber, currentLine));
i = 0;
while (attributes[i] != NULL) {
GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, (" [%d]='%s'", i, attributes[i]));
gf_free(attributes[i]);
attributes[i] = NULL;
i++;
}
GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("\n"));
gf_free(attributes);
attributes = NULL;
}
if (attribs.is_playlist_ended) {
(*playlist)->playlist_needs_refresh = GF_FALSE;
}
if (attribs.independant_segments) {
(*playlist)->independent_segments = GF_TRUE;
}
if (attribs.mediaURL) {
GF_Err e = declare_sub_playlist(attribs.mediaURL, baseURL, &attribs, sub_playlist, playlist, in_stream);
gf_free(attribs.mediaURL);
attribs.mediaURL = NULL;
if (e != GF_OK) {
if (f) gf_fclose(f);
return e;
}
}
}
} else {
GF_Err e = declare_sub_playlist(currentLine, baseURL, &attribs, sub_playlist, playlist, in_stream);
attribs.current_media_seq += 1;
if (e != GF_OK) {
if (f) gf_fclose(f);
return e;
}
attribs.width = attribs.height = 0;
attribs.codecs = NULL;
}
}
if (f) gf_fclose(f);
for (i=0; i<(int)gf_list_count((*playlist)->streams); i++) {
u32 j;
Stream *prog = gf_list_get((*playlist)->streams, i);
prog->computed_duration = 0;
for (j=0; j<gf_list_count(prog->variants); j++) {
PlaylistElement *ple = gf_list_get(prog->variants, j);
if (ple->element_type == TYPE_PLAYLIST) {
if (ple->element.playlist.computed_duration > prog->computed_duration)
prog->computed_duration = ple->element.playlist.computed_duration;
}
}
}
if (attribs.key_url)
gf_free(attribs.key_url);
if (attribs.init_url)
gf_free(attribs.init_url);
if (attribs.version < attribs.compatibility_version) {
GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] Version %d specified but tags from version %d detected\n", attribs.version, attribs.compatibility_version));
}
return GF_OK;
}