blob: 7622204fe5029a33e4ebf253cc1ca1a2a91bab23 [file] [edit]
/*
* GPAC - Multimedia Framework C SDK
*
* Authors: Pierre Souchay, Jean Le Feuvre, Romain Bouqueau
* Copyright (c) Telecom ParisTech 2010-2026
* All rights reserved
*
* This file is part of GPAC
*
* GPAC is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* GPAC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; see the file COPYING. If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
#define _GNU_SOURCE
#include <gpac/internal/m3u8.h>
#include <gpac/network.h>
/********** accumulated_attributes **********/
typedef struct _s_accumulated_attributes {
//TODO: store as a structure with: { attribute, version, mandatory }
char *title;
char *mediaURL;
double duration_in_seconds;
int bandwidth;
int width, height;
int stream_id;
char *codecs;
char *language;
char *name;
u32 channels;
MediaType type;
char *group_audio;
char *group_video;
char *group_subtitle;
char *group_closed_captions;
Bool forced;
int target_duration_in_seconds;
int min_media_sequence;
int current_media_seq;
u32 version;
u32 compatibility_version; /*compute version required by the M3U8 content*/
Bool is_master_playlist;
Bool is_media_segment;
Bool is_playlist_ended;
Bool is_default;
Bool is_autoselect;
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 has_iv;
Bool independent_segments;
Bool low_latency, independent_part;
u32 discontinuity;
} s_accumulated_attributes;
/********** playlist_element **********/
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;
}
/**
* Deletes an Playlist element
*/
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->name) {
gf_free(e->name);
}
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);
}
if (e->init_segment_url) {
gf_free(e->init_segment_url);
}
if (e->alt_bandwidths) {
gf_free(e->alt_bandwidths);
}
if (e->main_codecs) {
gf_free(e->main_codecs);
}
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:
gf_assert(e->element.playlist.elements);
result |= cleanup_list_of_elements(e->element.playlist.elements);
default:
break;
}
gf_free(e);
return result;
}
/**
* Creates an Playlist element.
* This element can be either a playlist of a stream according to first parameter.
* \return The PlaylistElement or NULL if it could not be created. Elements will be deleted recursively.
*/
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->low_lat_chunk = attribs->low_latency;
e->independent_chunk = attribs->independent_part;
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->name = (attribs->name ? gf_strdup(attribs->name) : 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;
if (e->drm_method) {
e->key_uri = NULL;
if (attribs->key_url) {
e->key_uri = gf_strdup(attribs->key_url);
}
if (attribs->has_iv) {
memcpy(e->key_iv, attribs->key_iv, sizeof(bin128));
} else {
u32 iv = gf_htonl(attribs->current_media_seq);
memset(e->key_iv, 0, sizeof(bin128) );
memcpy(e->key_iv + 12, (const void *) &iv, sizeof(iv));
}
}
e->utc_start_time = attribs->playlist_utc_timestamp;
e->discontinuity = attribs->discontinuity;
gf_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)) {
playlist_element_del(e);
return NULL;
}
} else {
/* Nothing to do, media is an empty structure */
}
gf_assert(e->bandwidth == 0);
gf_assert(e->url);
return e;
}
/********** stream **********/
/**
* Creates a new stream properly initialized
*/
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;
}
/**
* Deletes the specified stream
*/
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);
gf_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*));
if (!ret) return NULL;
if (!num_attributes) return ret;
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_malloc( (1+sz-spaces) * sizeof(char));
memcpy(ret[curr_attribute], &(line[start+spaces]), sz-spaces);
ret[curr_attribute][sz-spaces] = 0;
curr_attribute++;
if (curr_attribute >= num_attributes)
break;
}
}
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 void free_attrs(char** attributes)
{
u32 i = 0;
while (attributes[i] != NULL) {
gf_free(attributes[i]);
i++;
}
gf_free(attributes);
}
/**
* Parses the attributes and accumulate into the attributes structure
*/
static char** parse_attributes(const char *line, s_accumulated_attributes *attributes) {
int int_value, i;
char **ret;
char *end_ptr;
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;
}
/* reset not accumated attributes */
attributes->type = MEDIA_TYPE_UNKNOWN;
ret = extract_attributes("#EXT-X-TARGETDURATION:", line, 1);
if (ret) {
/* #EXT-X-TARGETDURATION:<seconds> */
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) {
/* #EXT-X-MEDIA-SEQUENCE:<number> */
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) {
/* #EXT-X-VERSION:<number> */
if (ret[0]) {
int_value = (s32)strtol(ret[0], &end_ptr, 10);
if (end_ptr != ret[0]) {
attributes->version = int_value;
}
//although technically it is mandated for v2 or more, don't complain if set for v1
M3U8_COMPATIBILITY_VERSION(1);
}
return ret;
}
ret = extract_attributes("#EXTINF:", line, 2);
if (ret) {
M3U8_COMPATIBILITY_VERSION(1);
/* #EXTINF:<duration>,<title> */
attributes->is_media_segment = GF_TRUE;
if (ret[0]) {
double 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]) {
if (attributes->title) gf_free(attributes->title);
attributes->title = gf_strdup(ret[1]);
}
return ret;
}
ret = extract_attributes("#EXT-X-KEY:", line, 4);
if (ret) {
/* #EXT-X-KEY:METHOD=<method>[,URI="<URI>"] */
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)) {
attributes->key_method = DRM_NONE;
if (attributes->key_url) {
gf_free(attributes->key_url);
attributes->key_url = NULL;
}
} else if (!strncmp(ret[0]+method_len, "AES-128", 7)) {
attributes->key_method = DRM_AES_128;
} else if (!strncmp(ret[0]+method_len, "SAMPLE-AES", 10)) {
attributes->key_method = DRM_CENC_CBCS;
} else if (!strncmp(ret[0]+method_len, "SAMPLE-AES-CTR", 14)) {
attributes->key_method = DRM_CENC_CTR;
} else {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] EXT-X-KEY method not recognized.\n"));
}
if (ret[1] != NULL && safe_start_equals("URI=\"", ret[1])) {
int_value = (u32) strlen(ret[1]);
if (ret[1][int_value-1] == '"') {
if (attributes->key_url) gf_free(attributes->key_url);
attributes->key_url = gf_strdup(&(ret[1][5]));
if (attributes->key_url) {
u32 klen = (u32) strlen(attributes->key_url);
attributes->key_url[klen ? klen-1 : 0] = 0;
}
}
}
attributes->has_iv = GF_FALSE;
if (ret[2] != NULL && safe_start_equals("IV=", ret[2])) {
char *IV = ret[2] + 3;
if (!strncmp(IV, "0x", 2)) IV+=2;
if (strlen(IV) != 32) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] EXT-X-KEY wrong IV len\n"));
} else {
for (i=0; i<16; i++) {
char szV[3];
u32 v;
szV[0] = IV[2*i];
szV[1] = IV[2*i + 1];
szV[2] = 0;
sscanf(szV, "%X", &v);
attributes->key_iv[i] = v;
}
}
attributes->has_iv = GF_TRUE;
}
}
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-PROGRAM-DATE-TIME:", line, 1);
if (ret) {
/* #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ> */
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) {
/* #EXT-X-ALLOW-CACHE:<YES|NO> */
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) {
/* #EXT-X-MAP:URI="<URI>"] */
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 (int_value > 0 && uri[int_value-1] == '"') {
if (attributes->init_url) gf_free(attributes->init_url);
attributes->init_url = gf_strdup(uri);
attributes->init_url[int_value-1]=0;
} else {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid URI (%s) in EXT-X-MAP\n", val));
}
}
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) {
/* #EXT-X-STREAM-INF:[attribute=value][,attribute=value]* */
i = 0;
attributes->is_master_playlist = GF_TRUE;
M3U8_COMPATIBILITY_VERSION(1);
while (ret[i] != NULL) {
char *utility;
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] == '"') {
if (attributes->codecs) gf_free(attributes->codecs);
attributes->codecs = gf_strdup(&(ret[i][8]));
if (attributes->codecs[0]) attributes->codecs[strlen(attributes->codecs)-1] = 0;
}
} 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])) {
gf_assert(attributes->type == MEDIA_TYPE_UNKNOWN);
attributes->type = MEDIA_TYPE_AUDIO;
if (attributes->group_audio) gf_free(attributes->group_audio);
attributes->group_audio = gf_strdup(ret[i] + 6);
M3U8_COMPATIBILITY_VERSION(4);
} else if (safe_start_equals("VIDEO=", ret[i])) {
gf_assert(attributes->type == MEDIA_TYPE_UNKNOWN);
attributes->type = MEDIA_TYPE_VIDEO;
if (attributes->group_video) gf_free(attributes->group_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"));
free_attrs(ret);
return NULL;
}
return ret;
}
if (!strcmp(line, "#EXT-X-DISCONTINUITY") ) {
attributes->discontinuity += 1;
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-DISCONTINUITY-SEQUENCE:", line, 1);
if (ret) {
if (ret[0]) {
int_value = (s32)strtol(ret[0], &end_ptr, 10);
if (end_ptr != ret[0]) {
attributes->discontinuity = int_value;
}
}
M3U8_COMPATIBILITY_VERSION(1);
return ret;
}
ret = extract_attributes("#EXT-X-BYTERANGE:", line, 1);
if (ret) {
/* #EXT-X-BYTERANGE:<begin@end> */
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) {
/* #EXT-X-MEDIA:[TYPE={AUDIO,VIDEO}],[URI],[GROUP-ID],[LANGUAGE],[NAME],[DEFAULT={YES,NO}],[AUTOSELECT={YES,NO}] */
M3U8_COMPATIBILITY_VERSION(4);
attributes->is_master_playlist = GF_TRUE;
attributes->bandwidth = 0;
attributes->width = 0;
attributes->height = 0;
attributes->channels = 0;
attributes->forced = GF_FALSE;
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;
if (attributes->mediaURL) gf_free(attributes->mediaURL);
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) {
if (attributes->group_audio) gf_free(attributes->group_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) {
if (attributes->group_video) gf_free(attributes->group_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) {
if (attributes->group_subtitle) gf_free(attributes->group_subtitle);
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) {
if (attributes->group_closed_captions) gf_free(attributes->group_closed_captions);
attributes->group_closed_captions = gf_strdup(ret[i]+9);
attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(CLOSED_CAPTIONS, attributes->group_closed_captions);
} 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));
free_attrs(ret);
return NULL;
}
} else if (safe_start_equals("LANGUAGE=\"", ret[i])) {
size_t len;
u32 offset=9;
if (ret[i][9] == '"') offset++;
if (attributes->language) gf_free(attributes->language);
attributes->language = gf_strdup(ret[i]+offset);
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])) {
if (attributes->name) gf_free(attributes->name);
attributes->name = gf_strdup(ret[i]+5+1);
u32 len = (u32) strlen(attributes->name);
if (len) attributes->name[len-1]=0;
} else if (safe_start_equals("DEFAULT=", ret[i])) {
if (!strncmp(ret[i]+8, "YES", 3)) {
attributes->is_default = GF_TRUE;
} else if (!strncmp(ret[i]+8, "NO", 2)) {
attributes->is_default = GF_FALSE;
} 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])) {
if (!strncmp(ret[i]+11, "YES", 3)) {
attributes->is_autoselect = GF_TRUE;
} else if (!strncmp(ret[i]+11, "NO", 2)) {
attributes->is_autoselect = GF_TRUE;
} else {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:AUTOSELECT=%s\n", ret[i]+11));
}
} else if (safe_start_equals("CHANNELS=", ret[i])) {
sscanf(ret[i] + 9, "\"%u\"", &attributes->channels);
} else if (safe_start_equals("INSTREAM-ID=", ret[i])) {
//we don't signal CC for now
} else if (safe_start_equals("FORCED=", ret[i])) {
attributes->forced = !stricmp(ret[i] + 7, "yes") ? GF_TRUE : GF_FALSE;
} else {
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Attribute %s not supported\n", ret[i]));
}
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"));
free_attrs(ret);
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"));
free_attrs(ret);
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"));
free_attrs(ret);
return NULL;
}
return ret;
}
if (!strncmp(line, "#EXT-X-INDEPENDENT-SEGMENTS", strlen("#EXT-X-INDEPENDENT-SEGMENTS") )) {
attributes->independent_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") )) {
//todo extract I/intra rate for speed adaptation
return NULL;
}
//ignored for now
if (!strncmp(line, "#EXT-X-BITRATE", strlen("#EXT-X-BITRATE") )) {
return NULL;
}
if (!strncmp(line, "#EXT-X-PART-INF", strlen("#EXT-X-PART-INF") )) {
attributes->low_latency = GF_TRUE;
return NULL;
}
//TODO for now we don't use preload hint
if (!strncmp(line, "#EXT-X-SERVER-CONTROL", strlen("#EXT-X-SERVER-CONTROL") )) {
return NULL;
}
//TODO for now we don't use preload hint
if (!strncmp(line, "#EXT-X-PRELOAD-HINT", strlen("#EXT-X-PRELOAD-HINT") )) {
return NULL;
}
//TODO for now we don't use preload hint
if (!strncmp(line, "#EXT-X-RENDITION-REPORT", strlen("#EXT-X-RENDITION-REPORT") )) {
return NULL;
}
//TODO for now we don't support interstitials
if (!strncmp(line, "#EXT-X-DATERANGE", strlen("#EXT-X-DATERANGE") )) {
return NULL;
}
GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Unsupported directive %s\n", line));
return NULL;
}
/**
* Creates a new MasterPlaylist
\return NULL if MasterPlaylist element could not be allocated
*/
MasterPlaylist* master_playlist_new()
{
MasterPlaylist *pl;
GF_SAFEALLOC(pl, 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;
}
/********** master_playlist **********/
GF_Err gf_m3u8_master_playlist_del(MasterPlaylist **playlist) {
if ((playlist == NULL) || (*playlist == NULL))
return GF_OK;
gf_assert((*playlist)->streams);
while (gf_list_count((*playlist)->streams)) {
Stream *p = gf_list_get((*playlist)->streams, 0);
while (p && gf_list_count(p->variants)) {
PlaylistElement *pl = gf_list_get(p->variants, 0);
if (!pl) break;
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;
gf_assert(pl);
gf_assert(pl->streams);
gf_assert(stream_id >= 0);
count = gf_list_count(pl->streams);
for (i=0; i<count; i++) {
Stream *cur = gf_list_get(pl->streams, i);
gf_assert(cur);
if (stream_id == cur->stream_id) {
/* We found the program */
return cur;
}
}
return NULL;
}
/********** sub_playlist **********/
#define M3U8_BUF_SIZE 2048
GF_EXPORT
GF_Err gf_m3u8_parse_master_playlist(const char *file, MasterPlaylist **playlist, const char *baseURL)
{
#ifdef GPAC_ENABLE_COVERAGE
if (gf_sys_is_cov_mode()) {
string2num("coverage");
}
#endif
return gf_m3u8_parse_sub_playlist(file, playlist, baseURL, NULL, NULL, GF_TRUE);
}
GF_Err declare_sub_playlist(char *currentLine, const char *baseURL, s_accumulated_attributes *attribs, PlaylistElement *sub_playlist, MasterPlaylist **playlist, Stream *in_stream)
{
u32 i, count;
char *fullURL = currentLine;
if (!fullURL) return GF_BAD_PARAM;
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;
}
GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] declaring %s %s\n", attribs->is_master_playlist ? "sub-playlist" : "media segment", fullURL));
{
PlaylistElement *curr_playlist = sub_playlist;
/* First, we have to find the matching stream */
Stream *stream = in_stream;
if (!in_stream)
stream = master_playlist_find_matching_stream(*playlist, attribs->stream_id);
/* We did not found the stream, we create it */
if (stream == NULL) {
stream = stream_new(attribs->stream_id);
if (stream == NULL) {
/* out of memory */
gf_m3u8_master_playlist_del(playlist);
return GF_OUT_OF_MEM;
}
gf_list_add((*playlist)->streams, stream);
/* take the first regular variant stream */
if ((*playlist)->current_stream < 0 && stream->stream_id < MEDIA_TYPE_AUDIO)
(*playlist)->current_stream = stream->stream_id;
}
/* OK, we have a stream, we have to choose the elements within the same stream variant */
gf_assert(stream);
gf_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);
gf_assert(i_playlist_element);
if (stream->stream_id < MEDIA_TYPE_AUDIO) {
/* regular stream (EXT-X-STREAM-INF) */
// Two stream are identical only if they have the same URL
if (attribs->is_media_segment || !strcmp(i_playlist_element->url, fullURL)) {
curr_playlist = i_playlist_element;
break;
}
} else {
/* group streams (EXT-X-MEDIA) */
//TODO: add renditions and compare depending on context parameters
}
}
}
/* We are the Master Playlist */
if (attribs->is_master_playlist) {
if (curr_playlist != NULL) {
//playlist has already been defined - this happens when the same video playlist is defined several times with different audio codecs ...
if (!attribs->codecs || !curr_playlist->audio_group || !attribs->group_audio || strstr(curr_playlist->audio_group, attribs->group_audio))
return GF_OK;
//gather codecs and bandwidth so that we can recompute them when generating the MPD
if (!curr_playlist->alt_bandwidths) {
curr_playlist->nb_alt_bandwidths = 1;
curr_playlist->alt_bandwidths = gf_malloc(sizeof(u32));
curr_playlist->alt_bandwidths[0] = curr_playlist->bandwidth;
}
char *codec = attribs->codecs;
while (codec) {
char *sep = strchr(codec, ',');
if (sep) sep[0] = 0;
if (!curr_playlist->codecs)
gf_dynstrcat(&curr_playlist->codecs, codec, NULL);
else if (!strstr(curr_playlist->codecs, codec))
gf_dynstrcat(&curr_playlist->codecs, codec, ",");
else if (!curr_playlist->main_codecs || !strstr(curr_playlist->main_codecs, codec))
gf_dynstrcat(&curr_playlist->main_codecs, codec, ",");
if (!sep) break;
sep[0] = ',';
codec = sep+1;
}
gf_dynstrcat(&curr_playlist->audio_group, attribs->group_audio, ",");
curr_playlist->alt_bandwidths = gf_realloc(curr_playlist->alt_bandwidths, sizeof(u32)*(curr_playlist->nb_alt_bandwidths+1) );
curr_playlist->alt_bandwidths[curr_playlist->nb_alt_bandwidths] = attribs->bandwidth;
curr_playlist->nb_alt_bandwidths++;
return GF_OK;
}
curr_playlist = playlist_element_new(TYPE_PLAYLIST, fullURL, attribs);
if (curr_playlist == NULL) {
/* out of memory */
gf_m3u8_master_playlist_del(playlist);
return GF_OUT_OF_MEM;
}
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."));
}
curr_playlist->audio_group = attribs->group_audio ? gf_strdup(attribs->group_audio) : NULL;
gf_list_add(stream->variants, curr_playlist);
curr_playlist->width = attribs->width;
curr_playlist->height = attribs->height;
curr_playlist->channels = attribs->channels;
} else {
/* Normal Playlist */
gf_assert((*playlist)->streams);
if (curr_playlist == NULL) {
/* This is a "normal" playlist without any element in it */
PlaylistElement *subElement;
gf_assert(baseURL);
curr_playlist = playlist_element_new(TYPE_PLAYLIST, baseURL, attribs);
if (curr_playlist == NULL) {
/* out of memory */
gf_m3u8_master_playlist_del(playlist);
return GF_OUT_OF_MEM;
}
gf_assert(curr_playlist->element.playlist.elements);
gf_assert(curr_playlist->url && !curr_playlist->codecs);
curr_playlist->codecs = NULL;
subElement = playlist_element_new(TYPE_UNKNOWN, fullURL, attribs);
if (attribs->init_url) {
gf_free(attribs->init_url);
attribs->init_url = NULL;
}
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;
gf_assert(stream);
gf_assert(stream->variants);
gf_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;
/* We first set the default duration for element, aka targetDuration */
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) {
/* we set the playlist duration info as the duration of a segment, only if it's not set
There are cases of playlist with the last segment with a duration different from the others
(example: Apple bipbop test)*/
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;
curr_playlist->element.playlist.discontinuity = attribs->discontinuity;
if (attribs->bandwidth > 1)
curr_playlist->bandwidth = attribs->bandwidth;
if (attribs->is_playlist_ended)
curr_playlist->element.playlist.is_ended = GF_TRUE;
}
/* Cleanup all line-specific fields */
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;
attribs->is_default = 0;
attribs->is_autoselect = 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->name != NULL) {
gf_free(attribs->name);
attribs->name = 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 (attribs->group_subtitle != NULL) {
gf_free(attribs->group_subtitle);
attribs->group_subtitle = NULL;
}
if (attribs->group_closed_captions != NULL) {
gf_free(attribs->group_closed_captions);
attribs->group_closed_captions = NULL;
}
return GF_OK;
}
typedef struct
{
char *name;
u64 start;
u32 size;
Double duration;
} HLS_LLChunk;
static void reset_attribs(s_accumulated_attributes *attribs, Bool is_cleanup)
{
attribs->width = attribs->height = 0;
#define RST_ATTR(_name) if (attribs->_name) { gf_free(attribs->_name); attribs->_name = NULL; }
RST_ATTR(codecs)
RST_ATTR(group_audio)
RST_ATTR(group_video)
RST_ATTR(group_subtitle)
RST_ATTR(group_closed_captions)
RST_ATTR(language)
RST_ATTR(title)
if (is_cleanup) {
RST_ATTR(key_url)
RST_ATTR(name)
}
RST_ATTR(init_url)
RST_ATTR(mediaURL)
}
GF_Err gf_m3u8_parse_sub_playlist(const char *m3u8_file, MasterPlaylist **playlist, const char *baseURL, Stream *in_stream, PlaylistElement *sub_playlist, Bool is_master)
{
int i, currentLineNumber;
FILE *f = NULL;
u8 *m3u8_payload;
u32 m3u8_size, m3u8pos;
char line_buffer[M3U8_BUF_SIZE];
char *currentLine = line_buffer;
size_t line_capacity = sizeof(line_buffer);
char **attributes = NULL;
Bool release_blob = GF_FALSE;
s_accumulated_attributes attribs;
if (!strncmp(m3u8_file, "gmem://", 7)) {
GF_Err e = gf_blob_get(m3u8_file, &m3u8_payload, &m3u8_size, NULL);
if (e) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot Open m3u8 source %s for reading\n", m3u8_file));
return e;
}
release_blob = GF_TRUE;
} else {
f = gf_fopen(m3u8_file, "rt");
if (!f) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot open m3u8 file %s for reading\n", m3u8_file));
return GF_URL_ERROR;
}
}
memset(&attribs, 0, sizeof(s_accumulated_attributes));
#define _CLEANUP \
reset_attribs(&attribs, GF_TRUE);\
if (currentLine != line_buffer) gf_free(currentLine); \
if (f) gf_fclose(f); \
else if (release_blob) gf_blob_release(m3u8_file);
if (*playlist == NULL) {
*playlist = master_playlist_new();
if (!(*playlist)) {
_CLEANUP
return GF_OUT_OF_MEM;
}
}
currentLineNumber = 0;
reset_attributes(&attribs);
m3u8pos = 0;
while (1) {
char *eof;
int len;
if (f) {
if (!gf_fgets(currentLine, sizeof(line_buffer), f))
break;
} else {
u32 line_start = m3u8pos;
size_t line_size;
if (m3u8pos >= m3u8_size)
break;
/* Keep a complete logical line, including its terminating newline. */
while (m3u8pos < m3u8_size) {
char c = m3u8_payload[m3u8pos++];
if ((c=='\n') || (c=='\r')) break;
}
line_size = m3u8pos - line_start;
if (line_size >= line_capacity) {
char *new_line = NULL;
/* Reserve the NUL without overflowing size_t on 32-bit builds. */
if (line_size != (size_t) -1)
new_line = gf_realloc(currentLine == line_buffer ? NULL : currentLine, line_size + 1);
if (!new_line) {
_CLEANUP
return GF_OUT_OF_MEM;
}
currentLine = new_line;
line_capacity = line_size + 1;
}
memcpy(currentLine, m3u8_payload + line_start, line_size);
currentLine[line_size] = 0;
}
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) {
/* Playlist MUST start with #EXTM3U */
if (len < 7 || (strncmp("#EXTM3U", currentLine, 7) != 0)) {
GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("Failed to parse M3U8 File, it should start with #EXTM3U, but was : %s\n", currentLine));
_CLEANUP
return GF_STREAM_NOT_FOUND;
}
continue;
}
if (currentLine[0] == '#') {
/* chunk */
if (!strncmp("#EXT-X-PART:", currentLine, 12)) {
GF_Err e = GF_NON_COMPLIANT_BITSTREAM;
char *sep;
char *file = strstr(currentLine, "URI=\"");
char *dur = strstr(currentLine, "DURATION=");
char *br = strstr(currentLine, "BYTERANGE=");
if (strstr(currentLine, "INDEPENDENT=YES")) {
attribs.independent_part = GF_TRUE;
}
if (br) {
u64 start=0;
u32 size=0;
sep = strchr(br, ',');
if (sep) sep[0] = 0;
if (sscanf(br+10, "\"%u@"LLU"\"", &size, &start) != 2)
file = NULL;
attribs.byte_range_start = start;
attribs.byte_range_end = start + size - 1;
}
if (dur) {
sep = strchr(dur, ',');
if (sep) sep[0] = 0;
attribs.duration_in_seconds = atof(dur+9);
}
if (file && dur) {
file += 5; // file starts with `URI:"`, move to start of URL
//find end quote
sep = strchr(file, '"');
if (!sep) {
e = GF_NON_COMPLIANT_BITSTREAM;
_CLEANUP
return e;
}
sep[0] = 0;
attribs.low_latency = GF_TRUE;
attribs.is_media_segment = GF_TRUE;
e = declare_sub_playlist(file, baseURL, &attribs, sub_playlist, playlist, in_stream);
(*playlist)->low_latency = GF_TRUE;
sep[0] = '"';
}
attribs.is_media_segment = GF_FALSE;
attribs.low_latency = GF_FALSE;
attribs.independent_part = GF_FALSE;
attribs.byte_range_start = attribs.byte_range_end = 0;
attribs.duration_in_seconds = 0;
if (e != GF_OK) {
_CLEANUP
return e;
}
}
/* A comment or a directive */
else if (!strncmp("#EXT", currentLine, 4)) {
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.independent_segments) {
(*playlist)->independent_segments = GF_TRUE;
}
if (attribs.low_latency) {
(*playlist)->low_latency = GF_TRUE;
attribs.low_latency = GF_FALSE;
}
if (attribs.version > (*playlist)->version) {
(*playlist)->version = attribs.version;
}
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) {
_CLEANUP
return e;
}
}
}
} else {
/*file encountered: sub-playlist or segment*/
GF_Err e;
const char *pl_url = baseURL;
//this is the first (master) playlist but what is parsed is not a master playlist
//we will create a master + child with xlink, but the xlink must be only the file name
//otherwise dir/pl_video.m3u8 will be translated in master(dir/pl_video.m3u8) [ child(xlink=dir/pl_video.m3u8) ]
//thus xlink resolution would give dir/dir/pl_video.m3u8
if (is_master && !attribs.is_master_playlist)
pl_url = gf_file_basename(baseURL);
e = declare_sub_playlist(currentLine, pl_url, &attribs, sub_playlist, playlist, in_stream);
attribs.current_media_seq += 1;
if (e != GF_OK) {
_CLEANUP
return e;
}
//do not reset all attributes but at least set width/height/codecs to NULL, otherwise we may miss detection
//of audio-only playlists in av sequences
reset_attribs(&attribs, GF_FALSE);
}
}
_CLEANUP
#undef _CLEANUP
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.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;
}