Coverage Report

Created: 2026-06-15 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gpac/src/media_tools/m3u8.c
Line
Count
Source
1
/*
2
 *      GPAC - Multimedia Framework C SDK
3
 *
4
 *      Authors: Pierre Souchay, Jean Le Feuvre, Romain Bouqueau
5
 *      Copyright (c) Telecom ParisTech 2010-2026
6
 *          All rights reserved
7
 *
8
 *  This file is part of GPAC
9
 *
10
 *  GPAC is free software; you can redistribute it and/or modify
11
 *  it under the terms of the GNU Lesser General Public License as published by
12
 *  the Free Software Foundation; either version 2, or (at your option)
13
 *  any later version.
14
 *
15
 *  GPAC is distributed in the hope that it will be useful,
16
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 *  GNU Lesser General Public License for more details.
19
 *
20
 *  You should have received a copy of the GNU Lesser General Public
21
 *  License along with this library; see the file COPYING.  If not, write to
22
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
23
 *
24
 */
25
26
#define _GNU_SOURCE
27
28
#include <gpac/internal/m3u8.h>
29
#include <gpac/network.h>
30
31
/********** accumulated_attributes **********/
32
33
typedef struct _s_accumulated_attributes {
34
  //TODO: store as a structure with: { attribute, version, mandatory }
35
  char *title;
36
  char *mediaURL;
37
  double duration_in_seconds;
38
  int bandwidth;
39
  int width, height;
40
  int stream_id;
41
  char *codecs;
42
  char *language;
43
  char *name;
44
  u32 channels;
45
  MediaType type;
46
  char *group_audio;
47
  char *group_video;
48
  char *group_subtitle;
49
  char *group_closed_captions;
50
  Bool forced;
51
52
  int target_duration_in_seconds;
53
  int min_media_sequence;
54
  int current_media_seq;
55
  u32 version;
56
  u32 compatibility_version; /*compute version required by the M3U8 content*/
57
  Bool is_master_playlist;
58
  Bool is_media_segment;
59
  Bool is_playlist_ended;
60
  Bool is_default;
61
  Bool is_autoselect;
62
  u64 playlist_utc_timestamp;
63
  u64 byte_range_start, byte_range_end;
64
  u64 init_byte_range_start, init_byte_range_end;
65
  PlaylistElementDRMMethod key_method;
66
  char *init_url;
67
  char *key_url;
68
  bin128 key_iv;
69
  Bool has_iv;
70
  Bool independent_segments;
71
  Bool low_latency, independent_part;
72
  u32 discontinuity;
73
} s_accumulated_attributes;
74
75
76
/********** playlist_element **********/
77
78
GF_Err playlist_element_del(PlaylistElement * e);
79
80
0
static GF_Err cleanup_list_of_elements(GF_List *list) {
81
0
  GF_Err result = GF_OK;
82
0
  if (list == NULL)
83
0
    return result;
84
0
  while (gf_list_count(list)) {
85
0
    PlaylistElement *pl = (PlaylistElement *) gf_list_get(list, 0);
86
0
    if (pl)
87
0
      result |= playlist_element_del(pl);
88
0
    gf_list_rem(list, 0);
89
0
  }
90
0
  gf_list_del(list);
91
0
  return result;
92
0
}
93
94
/**
95
 * Deletes an Playlist element
96
 */
97
0
GF_Err playlist_element_del(PlaylistElement * e) {
98
0
  GF_Err result = GF_OK;
99
0
  if (e == NULL)
100
0
    return result;
101
0
  if (e->title) {
102
0
    gf_free(e->title);
103
0
  }
104
0
  if (e->codecs) {
105
0
    gf_free(e->codecs);
106
0
  }
107
0
  if (e->language) {
108
0
    gf_free(e->language);
109
0
  }
110
0
  if (e->name) {
111
0
    gf_free(e->name);
112
0
  }
113
0
  if (e->audio_group) {
114
0
    gf_free(e->audio_group);
115
0
  }
116
0
  if (e->video_group) {
117
0
    gf_free(e->video_group);
118
0
  }
119
0
  if (e->key_uri) {
120
0
    gf_free(e->key_uri);
121
0
  }
122
0
  if (e->init_segment_url) {
123
0
    gf_free(e->init_segment_url);
124
0
  }
125
0
  if (e->alt_bandwidths) {
126
0
    gf_free(e->alt_bandwidths);
127
0
  }
128
0
  if (e->main_codecs) {
129
0
    gf_free(e->main_codecs);
130
0
  }
131
0
  memset(e->key_iv, 0, sizeof(bin128) );
132
0
  if (e->url)
133
0
    gf_free(e->url);
134
135
0
  switch (e->element_type) {
136
0
  case TYPE_UNKNOWN:
137
0
  case TYPE_MEDIA:
138
0
    break;
139
0
  case TYPE_PLAYLIST:
140
0
    gf_assert(e->element.playlist.elements);
141
0
    result |= cleanup_list_of_elements(e->element.playlist.elements);
142
0
  default:
143
0
    break;
144
0
  }
145
0
  gf_free(e);
146
0
  return result;
147
0
}
148
149
/**
150
 * Creates an Playlist element.
151
 * This element can be either a playlist of a stream according to first parameter.
152
 * \return The PlaylistElement or NULL if it could not be created. Elements will be deleted recursively.
153
 */
154
static PlaylistElement* playlist_element_new(PlaylistElementType element_type, const char *url, s_accumulated_attributes *attribs)
155
0
{
156
0
  PlaylistElement *e;
157
0
  GF_SAFEALLOC(e, PlaylistElement);
158
0
  if (e == NULL)
159
0
    return NULL;
160
161
0
  e->media_type = attribs->type;
162
0
  e->duration_info = attribs->duration_in_seconds;
163
0
  e->byte_range_start = attribs->byte_range_start;
164
0
  e->byte_range_end = attribs->byte_range_end;
165
0
  e->low_lat_chunk = attribs->low_latency;
166
0
  e->independent_chunk = attribs->independent_part;
167
168
0
  e->title = (attribs->title ? gf_strdup(attribs->title) : NULL);
169
0
  e->codecs = (attribs->codecs ? gf_strdup(attribs->codecs) : NULL);
170
0
  e->language = (attribs->language ? gf_strdup(attribs->language) : NULL);
171
0
  e->name = (attribs->name ? gf_strdup(attribs->name) : NULL);
172
0
  e->drm_method = attribs->key_method;
173
0
  e->init_segment_url = attribs->init_url ? gf_strdup(attribs->init_url) : NULL;
174
0
  e->init_byte_range_start = attribs->init_byte_range_start;
175
0
  e->init_byte_range_end = attribs->init_byte_range_end;
176
177
0
  if (e->drm_method) {
178
0
    e->key_uri = NULL;
179
0
    if (attribs->key_url) {
180
0
      e->key_uri = gf_strdup(attribs->key_url);
181
0
    }
182
183
0
    if (attribs->has_iv) {
184
0
      memcpy(e->key_iv, attribs->key_iv, sizeof(bin128));
185
0
    } else {
186
0
      u32 iv = gf_htonl(attribs->current_media_seq);
187
0
      memset(e->key_iv, 0, sizeof(bin128) );
188
0
      memcpy(e->key_iv + 12, (const void *) &iv, sizeof(iv));
189
0
    }
190
0
  }
191
192
0
  e->utc_start_time = attribs->playlist_utc_timestamp;
193
0
  e->discontinuity = attribs->discontinuity;
194
195
0
  gf_assert(url);
196
0
  e->url = gf_strdup(url);
197
0
  e->bandwidth = 0;
198
0
  e->element_type = element_type;
199
0
  if (element_type == TYPE_PLAYLIST) {
200
0
    e->element.playlist.is_ended = GF_FALSE;
201
0
    e->element.playlist.target_duration = attribs->duration_in_seconds;
202
0
    e->element.playlist.current_media_seq = 0;
203
0
    e->element.playlist.media_seq_min = 0;
204
0
    e->element.playlist.media_seq_max = 0;
205
0
    e->element.playlist.elements = gf_list_new();
206
0
    if (NULL == (e->element.playlist.elements)) {
207
0
      playlist_element_del(e);
208
0
      return NULL;
209
0
    }
210
0
  } else {
211
    /* Nothing to do, media is an empty structure */
212
0
  }
213
0
  gf_assert(e->bandwidth == 0);
214
0
  gf_assert(e->url);
215
0
  return e;
216
0
}
217
218
219
/********** stream **********/
220
221
/**
222
 * Creates a new stream properly initialized
223
 */
224
0
static Stream* stream_new(int stream_id) {
225
0
  Stream *program = (Stream *) gf_malloc(sizeof(Stream));
226
0
  if (program == NULL) {
227
0
    return NULL;
228
0
  }
229
0
  program->stream_id = stream_id;
230
0
  program->variants = gf_list_new();
231
0
  if (program->variants == NULL) {
232
0
    gf_free(program);
233
0
    return NULL;
234
0
  }
235
0
  return program;
236
0
}
237
238
/**
239
 * Deletes the specified stream
240
 */
241
0
static GF_Err stream_del(Stream *stream) {
242
0
  GF_Err e = GF_OK;
243
0
  if (stream == NULL)
244
0
    return e;
245
0
  if (stream->variants) {
246
0
    while (gf_list_count(stream->variants)) {
247
0
      GF_List *l = gf_list_get(stream->variants, 0);
248
0
      cleanup_list_of_elements(l);
249
0
      gf_list_rem(stream->variants, 0);
250
0
    }
251
0
    gf_list_del(stream->variants);
252
0
  }
253
0
  stream->variants = NULL;
254
0
  gf_free(stream);
255
0
  return e;
256
0
}
257
258
259
260
0
static GFINLINE int string2num(const char *s) {
261
0
  u64 ret=0, i, shift=2;
262
0
  u8 hash[GF_SHA1_DIGEST_SIZE];
263
0
  gf_sha1_csum((u8*)s, (u32)strlen(s), hash);
264
0
  gf_assert(shift*GF_SHA1_DIGEST_SIZE < 64);
265
0
  for (i=0; i<GF_SHA1_DIGEST_SIZE; ++i)
266
0
    ret += (ret << shift) + hash[i];
267
0
  return (int)(ret % MEDIA_TYPE_AUDIO);
268
0
}
269
270
271
0
#define GROUP_ID_TO_PROGRAM_ID(type, group_id) (\
272
0
  MEDIA_TYPE_##type + \
273
0
  string2num(group_id) \
274
0
  ) \
275
276
0
static Bool safe_start_equals(const char *attribute, const char *line) {
277
0
  size_t len, atlen;
278
0
  if (line == NULL)
279
0
    return GF_FALSE;
280
0
  len = strlen(line);
281
0
  atlen = strlen(attribute);
282
0
  if (len < atlen)
283
0
    return GF_FALSE;
284
0
  return (0 == strncmp(attribute, line, atlen));
285
0
}
286
287
288
static void reset_attributes(s_accumulated_attributes *attributes)
289
0
{
290
0
  memset(attributes, 0, sizeof(s_accumulated_attributes));
291
0
  attributes->type = MEDIA_TYPE_UNKNOWN;
292
0
  attributes->min_media_sequence = 1;
293
0
  attributes->version = 1;
294
0
  attributes->compatibility_version = 0;
295
0
  attributes->key_method = DRM_NONE;
296
0
}
297
298
0
static char** extract_attributes(const char *name, const char *line, const int num_attributes) {
299
0
  int sz, i, curr_attribute, start;
300
0
  char **ret;
301
0
  u8 quote = 0;
302
0
  int len = (u32) strlen(line);
303
0
  start = (u32) strlen(name);
304
0
  if (len <= start)
305
0
    return NULL;
306
0
  if (!safe_start_equals(name, line))
307
0
    return NULL;
308
0
  ret = gf_calloc((num_attributes + 1), sizeof(char*));
309
0
  if (!ret) return NULL;
310
0
  if (!num_attributes) return ret;
311
312
0
  curr_attribute = 0;
313
0
  for (i=start; i<=len; i++) {
314
0
    if (line[i] == '\0' || (!quote && line[i] == ',')  || (line[i] == quote)) {
315
0
      u32 spaces = 0;
316
0
      sz = i - start;
317
0
      if (quote && (line[i] == quote))
318
0
        sz++;
319
320
0
      while (line[start+spaces] == ' ')
321
0
        spaces++;
322
0
      if ((sz-spaces<=1) && (line[start+spaces]==',')) {
323
        //start = i+1;
324
0
      } else {
325
0
        if (!strncmp(&line[start+spaces], "\t", sz-spaces) || !strncmp(&line[start+spaces], "\n", sz-spaces)) {
326
0
        } else {
327
0
          ret[curr_attribute] = gf_calloc( (1+sz-spaces), sizeof(char));
328
0
          strncpy(ret[curr_attribute], &(line[start+spaces]), sz-spaces);
329
0
          curr_attribute++;
330
0
          if (curr_attribute >= num_attributes)
331
0
            break;
332
0
        }
333
0
      }
334
0
      start = i+1;
335
336
0
      if (start == len) {
337
0
        return ret;
338
0
      }
339
0
    }
340
0
    if ((line[i] == '\'') || (line[i] == '"'))  {
341
0
      if (quote) {
342
0
        quote = 0;
343
0
      } else {
344
0
        quote = line[i];
345
0
      }
346
0
    }
347
0
  }
348
0
  if (curr_attribute == 0) {
349
0
    gf_free(ret);
350
0
    return NULL;
351
0
  }
352
0
  return ret;
353
0
}
354
355
#define M3U8_COMPATIBILITY_VERSION(v) \
356
0
  if (v > attributes->compatibility_version) \
357
0
    attributes->compatibility_version = v;
358
359
static void free_attrs(char** attributes)
360
0
{
361
0
  u32 i = 0;
362
0
  while (attributes[i] != NULL) {
363
0
    gf_free(attributes[i]);
364
0
    i++;
365
0
  }
366
0
  gf_free(attributes);
367
0
}
368
/**
369
 * Parses the attributes and accumulate into the attributes structure
370
 */
371
0
static char** parse_attributes(const char *line, s_accumulated_attributes *attributes) {
372
0
  int int_value, i;
373
0
  char **ret;
374
0
  char *end_ptr;
375
0
  if (line == NULL)
376
0
    return NULL;
377
0
  if (!safe_start_equals("#EXT", line))
378
0
    return NULL;
379
0
  if (safe_start_equals("#EXT-X-ENDLIST", line)) {
380
0
    attributes->is_playlist_ended = GF_TRUE;
381
0
    M3U8_COMPATIBILITY_VERSION(1);
382
0
    return NULL;
383
0
  }
384
  /* reset not accumated attributes */
385
0
  attributes->type = MEDIA_TYPE_UNKNOWN;
386
387
0
  ret = extract_attributes("#EXT-X-TARGETDURATION:", line, 1);
388
0
  if (ret) {
389
    /* #EXT-X-TARGETDURATION:<seconds> */
390
0
    if (ret[0]) {
391
0
      int_value = (s32) strtol(ret[0], &end_ptr, 10);
392
0
      if (end_ptr != ret[0]) {
393
0
        attributes->target_duration_in_seconds = int_value;
394
0
      }
395
0
    }
396
0
    M3U8_COMPATIBILITY_VERSION(1);
397
0
    return ret;
398
0
  }
399
0
  ret = extract_attributes("#EXT-X-MEDIA-SEQUENCE:", line, 1);
400
0
  if (ret) {
401
    /* #EXT-X-MEDIA-SEQUENCE:<number> */
402
0
    if (ret[0]) {
403
0
      int_value = (s32)strtol(ret[0], &end_ptr, 10);
404
0
      if (end_ptr != ret[0]) {
405
0
        attributes->min_media_sequence = int_value;
406
0
        attributes->current_media_seq = int_value;
407
0
      }
408
0
    }
409
0
    M3U8_COMPATIBILITY_VERSION(1);
410
0
    return ret;
411
0
  }
412
0
  ret = extract_attributes("#EXT-X-VERSION:", line, 1);
413
0
  if (ret) {
414
    /* #EXT-X-VERSION:<number> */
415
0
    if (ret[0]) {
416
0
      int_value = (s32)strtol(ret[0], &end_ptr, 10);
417
0
      if (end_ptr != ret[0]) {
418
0
        attributes->version = int_value;
419
0
      }
420
      //although technically it is mandated for v2 or more, don't complain if set for v1
421
0
      M3U8_COMPATIBILITY_VERSION(1);
422
0
    }
423
0
    return ret;
424
0
  }
425
0
  ret = extract_attributes("#EXTINF:", line, 2);
426
0
  if (ret) {
427
0
    M3U8_COMPATIBILITY_VERSION(1);
428
    /* #EXTINF:<duration>,<title> */
429
0
    attributes->is_media_segment = GF_TRUE;
430
0
    if (ret[0]) {
431
0
      double double_value = strtod(ret[0], &end_ptr);
432
0
      if (end_ptr != ret[0]) {
433
0
        attributes->duration_in_seconds = double_value;
434
0
      }
435
0
      if (strstr(ret[0], ".") || (double_value > (int)double_value)) {
436
0
        M3U8_COMPATIBILITY_VERSION(3);
437
0
      }
438
0
    }
439
0
    if (ret[1]) {
440
0
      if (attributes->title) gf_free(attributes->title);
441
0
      attributes->title = gf_strdup(ret[1]);
442
0
    }
443
0
    return ret;
444
0
  }
445
0
  ret = extract_attributes("#EXT-X-KEY:", line, 4);
446
0
  if (ret) {
447
    /* #EXT-X-KEY:METHOD=<method>[,URI="<URI>"] */
448
0
    const char *method = "METHOD=";
449
0
    const size_t method_len = strlen(method);
450
0
    if (safe_start_equals(method, ret[0])) {
451
0
      if (!strncmp(ret[0]+method_len, "NONE", 4)) {
452
0
        attributes->key_method = DRM_NONE;
453
0
        if (attributes->key_url) {
454
0
          gf_free(attributes->key_url);
455
0
          attributes->key_url = NULL;
456
0
        }
457
0
      } else if (!strncmp(ret[0]+method_len, "AES-128", 7)) {
458
0
        attributes->key_method = DRM_AES_128;
459
0
      } else if (!strncmp(ret[0]+method_len, "SAMPLE-AES", 10)) {
460
0
        attributes->key_method = DRM_CENC_CBCS;
461
0
      } else if (!strncmp(ret[0]+method_len, "SAMPLE-AES-CTR", 14)) {
462
0
        attributes->key_method = DRM_CENC_CTR;
463
0
      } else {
464
0
        GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] EXT-X-KEY method not recognized.\n"));
465
0
      }
466
0
      if (ret[1] != NULL && safe_start_equals("URI=\"", ret[1])) {
467
0
        int_value = (u32) strlen(ret[1]);
468
0
        if (ret[1][int_value-1] == '"') {
469
0
          if (attributes->key_url) gf_free(attributes->key_url);
470
0
          attributes->key_url = gf_strdup(&(ret[1][5]));
471
0
          if (attributes->key_url) {
472
0
            u32 klen = (u32) strlen(attributes->key_url);
473
0
            attributes->key_url[klen ? klen-1 : 0] = 0;
474
0
          }
475
0
        }
476
0
      }
477
0
      attributes->has_iv = GF_FALSE;
478
0
      if (ret[2] != NULL && safe_start_equals("IV=", ret[2])) {
479
0
        char *IV = ret[2] + 3;
480
0
        if (!strncmp(IV, "0x", 2)) IV+=2;
481
0
        if (strlen(IV) != 32) {
482
0
          GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] EXT-X-KEY wrong IV len\n"));
483
0
        } else {
484
0
          for (i=0; i<16; i++) {
485
0
            char szV[3];
486
0
            u32 v;
487
0
            szV[0] = IV[2*i];
488
0
            szV[1] = IV[2*i + 1];
489
0
            szV[2] = 0;
490
0
            sscanf(szV, "%X", &v);
491
0
            attributes->key_iv[i] = v;
492
0
          }
493
0
        }
494
0
        attributes->has_iv = GF_TRUE;
495
0
      }
496
0
    }
497
0
    M3U8_COMPATIBILITY_VERSION(1);
498
0
    return ret;
499
0
  }
500
0
  ret = extract_attributes("#EXT-X-PROGRAM-DATE-TIME:", line, 1);
501
0
  if (ret) {
502
    /* #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ> */
503
0
    if (ret[0]) attributes->playlist_utc_timestamp = gf_net_parse_date(ret[0]);
504
0
    M3U8_COMPATIBILITY_VERSION(1);
505
0
    return ret;
506
0
  }
507
0
  ret = extract_attributes("#EXT-X-ALLOW-CACHE:", line, 1);
508
0
  if (ret) {
509
    /* #EXT-X-ALLOW-CACHE:<YES|NO> */
510
0
    GF_LOG(GF_LOG_INFO, GF_LOG_DASH,("[M3U8] EXT-X-ALLOW-CACHE not supported.\n", line));
511
0
    M3U8_COMPATIBILITY_VERSION(1);
512
0
    return ret;
513
0
  }
514
0
  ret = extract_attributes("#EXT-X-PLAYLIST-TYPE", line, 1);
515
0
  if (ret) {
516
0
    if (ret[0] && !strcmp(ret[0], "VOD")) attributes->is_playlist_ended = GF_TRUE;
517
0
    M3U8_COMPATIBILITY_VERSION(3);
518
0
    return ret;
519
0
  }
520
0
  ret = extract_attributes("#EXT-X-MAP", line, 4);
521
0
  if (ret) {
522
    /* #EXT-X-MAP:URI="<URI>"] */
523
0
    i=0;
524
0
    while (ret[i] != NULL) {
525
0
      char *val = ret[i];
526
0
      if (val[0]==':') val++;
527
0
      if (safe_start_equals("URI=\"", val)) {
528
0
        char *uri = val + 5;
529
0
        int_value = (u32) strlen(uri);
530
0
        if (int_value > 0 && uri[int_value-1] == '"') {
531
0
          if (attributes->init_url) gf_free(attributes->init_url);
532
0
          attributes->init_url = gf_strdup(uri);
533
0
          attributes->init_url[int_value-1]=0;
534
0
        } else {
535
0
          GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid URI (%s) in EXT-X-MAP\n", val));
536
0
        }
537
0
      }
538
0
      else if (safe_start_equals("BYTERANGE=\"", val)) {
539
0
        u64 begin, size;
540
0
        val+=10;
541
0
        if (sscanf(val, "\""LLU"@"LLU"\"", &size, &begin) == 2) {
542
0
          if (size) {
543
0
            attributes->init_byte_range_start = begin;
544
0
            attributes->init_byte_range_end = begin + size - 1;
545
0
          } else {
546
0
            GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid byte range %s\n", val));
547
0
          }
548
0
        }
549
0
      }
550
0
      i++;
551
0
    }
552
0
    M3U8_COMPATIBILITY_VERSION(3);
553
0
    return ret;
554
0
  }
555
0
  ret = extract_attributes("#EXT-X-STREAM-INF:", line, 10);
556
0
  if (ret) {
557
    /* #EXT-X-STREAM-INF:[attribute=value][,attribute=value]* */
558
0
    i = 0;
559
0
    attributes->is_master_playlist = GF_TRUE;
560
0
    M3U8_COMPATIBILITY_VERSION(1);
561
0
    while (ret[i] != NULL) {
562
0
      char *utility;
563
0
      if (safe_start_equals("BANDWIDTH=", ret[i])) {
564
0
        utility = &(ret[i][10]);
565
0
        int_value = (s32) strtol(utility, &end_ptr, 10);
566
0
        if (end_ptr != utility)
567
0
          attributes->bandwidth = int_value;
568
0
      } else if (safe_start_equals("PROGRAM-ID=", ret[i])) {
569
0
        utility = &(ret[i][11]);
570
0
        int_value = (s32) strtol(utility, &end_ptr, 10);
571
0
        if (end_ptr != utility)
572
0
          attributes->stream_id = int_value;
573
0
      } else if (safe_start_equals("CODECS=\"", ret[i])) {
574
0
        int_value = (u32) strlen(ret[i]);
575
0
        if (ret[i][int_value-1] == '"') {
576
0
          if (attributes->codecs) gf_free(attributes->codecs);
577
0
          attributes->codecs = gf_strdup(&(ret[i][8]));
578
0
          if (attributes->codecs[0]) attributes->codecs[strlen(attributes->codecs)-1] = 0;
579
0
        }
580
0
      } else if (safe_start_equals("RESOLUTION=", ret[i])) {
581
0
        u32 w, h;
582
0
        utility = &(ret[i][11]);
583
0
        if ((sscanf(utility, "%dx%d", &w, &h)==2) || (sscanf(utility, "%dx%d,", &w, &h)==2)) {
584
0
          attributes->width = w;
585
0
          attributes->height = h;
586
0
        }
587
0
        M3U8_COMPATIBILITY_VERSION(2);
588
0
      } else if (safe_start_equals("AUDIO=", ret[i])) {
589
0
        gf_assert(attributes->type == MEDIA_TYPE_UNKNOWN);
590
0
        attributes->type = MEDIA_TYPE_AUDIO;
591
0
        if (attributes->group_audio) gf_free(attributes->group_audio);
592
0
        attributes->group_audio = gf_strdup(ret[i] + 6);
593
0
        M3U8_COMPATIBILITY_VERSION(4);
594
0
      } else if (safe_start_equals("VIDEO=", ret[i])) {
595
0
        gf_assert(attributes->type == MEDIA_TYPE_UNKNOWN);
596
0
        attributes->type = MEDIA_TYPE_VIDEO;
597
0
        if (attributes->group_video) gf_free(attributes->group_video);
598
0
        attributes->group_video = gf_strdup(ret[i] + 6);
599
0
        M3U8_COMPATIBILITY_VERSION(4);
600
0
      }
601
0
      i++;
602
0
    }
603
0
    if (!attributes->bandwidth) {
604
0
      GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-STREAM-INF: no BANDWIDTH found. Ignoring the line.\n"));
605
0
      free_attrs(ret);
606
0
      return NULL;
607
0
    }
608
0
    return ret;
609
0
  }
610
0
  if (!strcmp(line, "#EXT-X-DISCONTINUITY") ) {
611
0
    attributes->discontinuity += 1;
612
0
    M3U8_COMPATIBILITY_VERSION(1);
613
0
    return ret;
614
0
  }
615
0
  ret = extract_attributes("#EXT-X-DISCONTINUITY-SEQUENCE:", line, 1);
616
0
  if (ret) {
617
0
    if (ret[0]) {
618
0
      int_value = (s32)strtol(ret[0], &end_ptr, 10);
619
0
      if (end_ptr != ret[0]) {
620
0
        attributes->discontinuity = int_value;
621
0
      }
622
0
    }
623
0
    M3U8_COMPATIBILITY_VERSION(1);
624
0
    return ret;
625
0
  }
626
0
  ret = extract_attributes("#EXT-X-BYTERANGE:", line, 1);
627
0
  if (ret) {
628
    /* #EXT-X-BYTERANGE:<begin@end> */
629
0
    if (ret[0]) {
630
0
      u64 begin, size;
631
0
      if (sscanf(ret[0], LLU"@"LLU, &size, &begin) == 2) {
632
0
        if (size) {
633
0
          attributes->byte_range_start = begin;
634
0
          attributes->byte_range_end = begin + size - 1;
635
0
        } else {
636
0
          GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid byte range %s\n", ret[0]));
637
0
        }
638
0
      }
639
0
    }
640
0
    M3U8_COMPATIBILITY_VERSION(4);
641
0
    return ret;
642
0
  }
643
0
  ret = extract_attributes("#EXT-X-MEDIA:", line, 14);
644
0
  if (ret) {
645
    /* #EXT-X-MEDIA:[TYPE={AUDIO,VIDEO}],[URI],[GROUP-ID],[LANGUAGE],[NAME],[DEFAULT={YES,NO}],[AUTOSELECT={YES,NO}] */
646
0
    M3U8_COMPATIBILITY_VERSION(4);
647
0
    attributes->is_master_playlist = GF_TRUE;
648
0
    attributes->bandwidth = 0;
649
0
    attributes->width = 0;
650
0
    attributes->height = 0;
651
0
    attributes->channels = 0;
652
0
    attributes->forced = GF_FALSE;
653
0
    i = 0;
654
0
    while (ret[i] != NULL) {
655
0
      if (safe_start_equals("TYPE=", ret[i])) {
656
0
        if (!strncmp(ret[i]+5, "AUDIO", 5)) {
657
0
          attributes->type = MEDIA_TYPE_AUDIO;
658
0
        } else if (!strncmp(ret[i]+5, "VIDEO", 5)) {
659
0
          attributes->type = MEDIA_TYPE_VIDEO;
660
0
        } else if (!strncmp(ret[i]+5, "SUBTITLES", 9)) {
661
0
          attributes->type = MEDIA_TYPE_SUBTITLES;
662
0
        } else if (!strncmp(ret[i]+5, "CLOSED-CAPTIONS", 15)) {
663
0
          attributes->type = MEDIA_TYPE_CLOSED_CAPTIONS;
664
0
        } else {
665
0
          GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Unsupported #EXT-X-MEDIA:TYPE=%s\n", ret[i]+5));
666
0
        }
667
0
      } else if (safe_start_equals("URI=\"", ret[i])) {
668
0
        size_t len;
669
0
        if (attributes->mediaURL) gf_free(attributes->mediaURL);
670
0
        attributes->mediaURL = gf_strdup(ret[i]+5);
671
0
        len = strlen(attributes->mediaURL);
672
0
        if (len && (attributes->mediaURL[len-1] == '"')) {
673
0
          attributes->mediaURL[len-1] = '\0';
674
0
        } else {
675
0
          GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Misformed #EXT-X-MEDIA:URI=%s. Quotes are incorrect.\n", ret[i]+5));
676
0
        }
677
0
      } else if (safe_start_equals("GROUP-ID=", ret[i])) {
678
0
        if (attributes->type == MEDIA_TYPE_AUDIO) {
679
0
          if (attributes->group_audio) gf_free(attributes->group_audio);
680
0
          attributes->group_audio = gf_strdup(ret[i]+9);
681
0
          attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(AUDIO, attributes->group_audio);
682
0
        } else if (attributes->type == MEDIA_TYPE_VIDEO) {
683
0
          if (attributes->group_video) gf_free(attributes->group_video);
684
0
          attributes->group_video = gf_strdup(ret[i]+9);
685
0
          attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(VIDEO, attributes->group_video);
686
0
        } else if (attributes->type == MEDIA_TYPE_SUBTITLES) {
687
0
          if (attributes->group_subtitle) gf_free(attributes->group_subtitle);
688
0
          attributes->group_subtitle = gf_strdup(ret[i]+9);
689
0
          attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(SUBTITLES, attributes->group_subtitle);
690
0
        } else if (attributes->type == MEDIA_TYPE_CLOSED_CAPTIONS) {
691
0
          if (attributes->group_closed_captions) gf_free(attributes->group_closed_captions);
692
0
          attributes->group_closed_captions = gf_strdup(ret[i]+9);
693
0
          attributes->stream_id = GROUP_ID_TO_PROGRAM_ID(CLOSED_CAPTIONS, attributes->group_closed_captions);
694
0
        } else if (attributes->type == MEDIA_TYPE_UNKNOWN) {
695
0
          GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:GROUP-ID=%s. Ignoring the line.\n", ret[i]+9));
696
0
          free_attrs(ret);
697
0
          return NULL;
698
0
        }
699
0
      } else if (safe_start_equals("LANGUAGE=\"", ret[i])) {
700
0
        size_t len;
701
0
        u32 offset=9;
702
0
        if (ret[i][9] == '"') offset++;
703
0
        if (attributes->language) gf_free(attributes->language);
704
0
        attributes->language = gf_strdup(ret[i]+offset);
705
0
        len = strlen(attributes->language);
706
0
        if (len && (attributes->language[len-1] == '"')) {
707
0
          attributes->language[len-1] = '\0';
708
0
        } else {
709
0
          GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Misformed #EXT-X-MEDIA:LANGUAGE=%s. Quotes are incorrect.\n", ret[i]+5));
710
0
        }
711
0
      } else if (safe_start_equals("NAME=\"", ret[i])) {
712
0
        if (attributes->name) gf_free(attributes->name);
713
0
        attributes->name = gf_strdup(ret[i]+5+1);
714
0
        u32 len = (u32) strlen(attributes->name);
715
0
        if (len) attributes->name[len-1]=0;
716
0
      } else if (safe_start_equals("DEFAULT=", ret[i])) {
717
0
        if (!strncmp(ret[i]+8, "YES", 3)) {
718
0
          attributes->is_default = GF_TRUE;
719
0
        } else if (!strncmp(ret[i]+8, "NO", 2)) {
720
0
          attributes->is_default = GF_FALSE;
721
0
        } else {
722
0
          GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:DEFAULT=%s\n", ret[i]+8));
723
0
        }
724
0
      } else if (safe_start_equals("AUTOSELECT=", ret[i])) {
725
0
        if (!strncmp(ret[i]+11, "YES", 3)) {
726
0
          attributes->is_autoselect = GF_TRUE;
727
0
        } else if (!strncmp(ret[i]+11, "NO", 2)) {
728
0
          attributes->is_autoselect = GF_TRUE;
729
0
        } else {
730
0
          GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA:AUTOSELECT=%s\n", ret[i]+11));
731
0
        }
732
0
      } else if (safe_start_equals("CHANNELS=", ret[i])) {
733
0
        sscanf(ret[i] + 9, "\"%u\"", &attributes->channels);
734
0
      } else if (safe_start_equals("INSTREAM-ID=", ret[i])) {
735
        //we don't signal CC for now
736
0
      } else if (safe_start_equals("FORCED=", ret[i])) {
737
0
        attributes->forced = !stricmp(ret[i] + 7, "yes") ? GF_TRUE : GF_FALSE;
738
0
      } else {
739
0
        GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Attribute %s not supported\n", ret[i]));
740
0
      }
741
0
      i++;
742
0
    }
743
744
0
    if (attributes->type == MEDIA_TYPE_UNKNOWN) {
745
0
      GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: TYPE is missing. Ignoring the line.\n"));
746
0
      free_attrs(ret);
747
0
      return NULL;
748
0
    }
749
0
    if (attributes->type == MEDIA_TYPE_CLOSED_CAPTIONS && attributes->mediaURL) {
750
0
      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"));
751
0
      gf_free(attributes->mediaURL);
752
0
      attributes->mediaURL = NULL;
753
0
    }
754
0
    if ((attributes->type == MEDIA_TYPE_AUDIO && !attributes->group_audio)
755
0
            || (attributes->type == MEDIA_TYPE_VIDEO && !attributes->group_video)) {
756
0
      GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: missing GROUP-ID attribute. Ignoring the line.\n"));
757
0
      free_attrs(ret);
758
0
      return NULL;
759
0
    }
760
0
    if (!attributes->stream_id) {
761
0
      GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Invalid #EXT-X-MEDIA: no ID was computed. Check previous errors. Ignoring the line.\n"));
762
0
      free_attrs(ret);
763
0
      return NULL;
764
0
    }
765
766
0
    return ret;
767
0
  }
768
0
  if (!strncmp(line, "#EXT-X-INDEPENDENT-SEGMENTS", strlen("#EXT-X-INDEPENDENT-SEGMENTS") )) {
769
0
    attributes->independent_segments = GF_TRUE;
770
0
    M3U8_COMPATIBILITY_VERSION(1);
771
0
    return NULL;
772
0
  }
773
0
  if (!strncmp(line, "#EXT-X-I-FRAME-STREAM-INF", strlen("#EXT-X-I-FRAME-STREAM-INF") )) {
774
    //todo extract I/intra rate for speed adaptation
775
0
    return NULL;
776
0
  }
777
  //ignored for now
778
0
  if (!strncmp(line, "#EXT-X-BITRATE", strlen("#EXT-X-BITRATE") )) {
779
0
    return NULL;
780
0
  }
781
0
  if (!strncmp(line, "#EXT-X-PART-INF", strlen("#EXT-X-PART-INF") )) {
782
0
    attributes->low_latency = GF_TRUE;
783
0
    return NULL;
784
0
  }
785
  //TODO for now we don't use preload hint
786
0
  if (!strncmp(line, "#EXT-X-SERVER-CONTROL", strlen("#EXT-X-SERVER-CONTROL") )) {
787
0
    return NULL;
788
0
  }
789
  //TODO for now we don't use preload hint
790
0
  if (!strncmp(line, "#EXT-X-PRELOAD-HINT", strlen("#EXT-X-PRELOAD-HINT") )) {
791
0
    return NULL;
792
0
  }
793
  //TODO for now we don't use preload hint
794
0
  if (!strncmp(line, "#EXT-X-RENDITION-REPORT", strlen("#EXT-X-RENDITION-REPORT") )) {
795
0
    return NULL;
796
0
  }
797
  //TODO for now we don't support interstitials
798
0
  if (!strncmp(line, "#EXT-X-DATERANGE", strlen("#EXT-X-DATERANGE") )) {
799
0
    return NULL;
800
0
  }
801
0
  GF_LOG(GF_LOG_WARNING, GF_LOG_DASH,("[M3U8] Unsupported directive %s\n", line));
802
0
  return NULL;
803
0
}
804
805
/**
806
 * Creates a new MasterPlaylist
807
\return NULL if MasterPlaylist element could not be allocated
808
 */
809
MasterPlaylist* master_playlist_new()
810
0
{
811
0
  MasterPlaylist *pl;
812
0
  GF_SAFEALLOC(pl, MasterPlaylist);
813
814
0
  if (pl == NULL)
815
0
    return NULL;
816
0
  pl->streams = gf_list_new();
817
0
  if (!pl->streams) {
818
0
    gf_free(pl);
819
0
    return NULL;
820
0
  }
821
0
  pl->current_stream = -1;
822
0
  pl->playlist_needs_refresh = GF_TRUE;
823
0
  return pl;
824
0
}
825
826
827
/********** master_playlist **********/
828
829
0
GF_Err gf_m3u8_master_playlist_del(MasterPlaylist **playlist) {
830
0
  if ((playlist == NULL) || (*playlist == NULL))
831
0
    return GF_OK;
832
0
  gf_assert((*playlist)->streams);
833
0
  while (gf_list_count((*playlist)->streams)) {
834
0
    Stream *p = gf_list_get((*playlist)->streams, 0);
835
0
    while (p && gf_list_count(p->variants)) {
836
0
      PlaylistElement *pl = gf_list_get(p->variants, 0);
837
0
      if (!pl) break;
838
0
      playlist_element_del(pl);
839
0
      gf_list_rem(p->variants, 0);
840
0
    }
841
0
    gf_list_del(p->variants);
842
0
    p->variants = NULL;
843
0
    stream_del(p);
844
0
    gf_list_rem((*playlist)->streams, 0);
845
0
  }
846
0
  gf_list_del((*playlist)->streams);
847
0
  (*playlist)->streams = NULL;
848
0
  gf_free(*playlist);
849
0
  *playlist = NULL;
850
851
0
  return GF_OK;
852
0
}
853
854
0
static Stream* master_playlist_find_matching_stream(const MasterPlaylist *pl, const u32 stream_id) {
855
0
  u32 count, i;
856
0
  gf_assert(pl);
857
0
  gf_assert(pl->streams);
858
0
  gf_assert(stream_id >= 0);
859
0
  count = gf_list_count(pl->streams);
860
0
  for (i=0; i<count; i++) {
861
0
    Stream *cur = gf_list_get(pl->streams, i);
862
0
    gf_assert(cur);
863
0
    if (stream_id == cur->stream_id) {
864
      /* We found the program */
865
0
      return cur;
866
0
    }
867
0
  }
868
0
  return NULL;
869
0
}
870
871
872
/********** sub_playlist **********/
873
874
0
#define M3U8_BUF_SIZE 2048
875
876
GF_EXPORT
877
GF_Err gf_m3u8_parse_master_playlist(const char *file, MasterPlaylist **playlist, const char *baseURL)
878
0
{
879
#ifdef GPAC_ENABLE_COVERAGE
880
  if (gf_sys_is_cov_mode()) {
881
    string2num("coverage");
882
  }
883
#endif
884
0
  return gf_m3u8_parse_sub_playlist(file, playlist, baseURL, NULL, NULL, GF_TRUE);
885
0
}
886
887
GF_Err declare_sub_playlist(char *currentLine, const char *baseURL, s_accumulated_attributes *attribs, PlaylistElement *sub_playlist, MasterPlaylist **playlist, Stream *in_stream)
888
0
{
889
0
  u32 i, count;
890
891
0
  char *fullURL = currentLine;
892
0
  if (!fullURL) return GF_BAD_PARAM;
893
894
0
  if (attribs->is_master_playlist && attribs->is_media_segment) {
895
0
    GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("[M3U8] Media segment tag MUST NOT appear in a Master Playlist\n"));
896
0
    return GF_BAD_PARAM;
897
0
  }
898
899
0
  GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] declaring %s %s\n", attribs->is_master_playlist ? "sub-playlist" : "media segment", fullURL));
900
901
0
  {
902
0
    PlaylistElement *curr_playlist = sub_playlist;
903
    /* First, we have to find the matching stream */
904
0
    Stream *stream = in_stream;
905
0
    if (!in_stream)
906
0
      stream = master_playlist_find_matching_stream(*playlist, attribs->stream_id);
907
    /* We did not found the stream, we create it */
908
0
    if (stream == NULL) {
909
0
      stream = stream_new(attribs->stream_id);
910
0
      if (stream == NULL) {
911
        /* out of memory */
912
0
        gf_m3u8_master_playlist_del(playlist);
913
0
        return GF_OUT_OF_MEM;
914
0
      }
915
0
      gf_list_add((*playlist)->streams, stream);
916
      /* take the first regular variant stream */
917
0
      if ((*playlist)->current_stream < 0 && stream->stream_id < MEDIA_TYPE_AUDIO)
918
0
        (*playlist)->current_stream = stream->stream_id;
919
0
    }
920
921
    /* OK, we have a stream, we have to choose the elements within the same stream variant */
922
0
    gf_assert(stream);
923
0
    gf_assert(stream->variants);
924
0
    count = gf_list_count(stream->variants);
925
926
0
    if (!curr_playlist) {
927
0
      for (i=0; i<(s32)count; i++) {
928
0
        PlaylistElement *i_playlist_element = gf_list_get(stream->variants, i);
929
0
        gf_assert(i_playlist_element);
930
0
        if (stream->stream_id < MEDIA_TYPE_AUDIO) {
931
          /* regular stream (EXT-X-STREAM-INF) */
932
          // Two stream are identical only if they have the same URL
933
0
          if (attribs->is_media_segment || !strcmp(i_playlist_element->url, fullURL)) {
934
0
            curr_playlist = i_playlist_element;
935
0
            break;
936
0
          }
937
0
        } else {
938
          /* group streams (EXT-X-MEDIA) */
939
          //TODO: add renditions and compare depending on context parameters
940
0
        }
941
0
      }
942
0
    }
943
944
    /* We are the Master Playlist */
945
0
    if (attribs->is_master_playlist) {
946
0
      if (curr_playlist != NULL) {
947
        //playlist has already been defined - this happens when the same video playlist is defined several times with different audio codecs ...
948
0
        if (!attribs->codecs || !curr_playlist->audio_group || !attribs->group_audio || strstr(curr_playlist->audio_group, attribs->group_audio))
949
0
          return GF_OK;
950
        //gather codecs and bandwidth so that we can recompute them when generating the MPD
951
0
        if (!curr_playlist->alt_bandwidths) {
952
0
          curr_playlist->nb_alt_bandwidths = 1;
953
0
          curr_playlist->alt_bandwidths = gf_malloc(sizeof(u32));
954
0
          curr_playlist->alt_bandwidths[0] = curr_playlist->bandwidth;
955
0
        }
956
0
        char *codec = attribs->codecs;
957
0
        while (codec) {
958
0
          char *sep = strchr(codec, ',');
959
0
          if (sep) sep[0] = 0;
960
0
          if (!curr_playlist->codecs)
961
0
            gf_dynstrcat(&curr_playlist->codecs, codec, NULL);
962
0
          else if (!strstr(curr_playlist->codecs, codec))
963
0
            gf_dynstrcat(&curr_playlist->codecs, codec, ",");
964
0
          else if (!curr_playlist->main_codecs || !strstr(curr_playlist->main_codecs, codec))
965
0
            gf_dynstrcat(&curr_playlist->main_codecs, codec, ",");
966
0
          if (!sep) break;
967
0
          sep[0] = ',';
968
0
          codec = sep+1;
969
0
        }
970
0
        gf_dynstrcat(&curr_playlist->audio_group, attribs->group_audio, ",");
971
0
        curr_playlist->alt_bandwidths = gf_realloc(curr_playlist->alt_bandwidths, sizeof(u32)*(curr_playlist->nb_alt_bandwidths+1) );
972
0
        curr_playlist->alt_bandwidths[curr_playlist->nb_alt_bandwidths] = attribs->bandwidth;
973
0
        curr_playlist->nb_alt_bandwidths++;
974
0
        return GF_OK;
975
0
      }
976
0
      curr_playlist = playlist_element_new(TYPE_PLAYLIST, fullURL, attribs);
977
0
      if (curr_playlist == NULL) {
978
        /* out of memory */
979
0
        gf_m3u8_master_playlist_del(playlist);
980
0
        return GF_OUT_OF_MEM;
981
0
      }
982
0
      if (curr_playlist->url)
983
0
        gf_free(curr_playlist->url);
984
0
      curr_playlist->url = gf_strdup(fullURL);
985
0
      if (curr_playlist->title)
986
0
        gf_free(curr_playlist->title);
987
0
      curr_playlist->title = attribs->title ? gf_strdup(attribs->title) : NULL;
988
0
      if (curr_playlist->codecs)
989
0
        gf_free(curr_playlist->codecs);
990
0
      curr_playlist->codecs = attribs->codecs ? gf_strdup(attribs->codecs) : NULL;
991
0
      if (curr_playlist->audio_group) {
992
0
        GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[M3U8] Warning: found an AUDIO group in the master playlist."));
993
0
      }
994
0
      if (curr_playlist->video_group) {
995
0
        GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[M3U8] Warning: found an VIDEO group in the master playlist."));
996
0
      }
997
0
      curr_playlist->audio_group = attribs->group_audio ? gf_strdup(attribs->group_audio) : NULL;
998
0
      gf_list_add(stream->variants, curr_playlist);
999
0
      curr_playlist->width = attribs->width;
1000
0
      curr_playlist->height = attribs->height;
1001
0
      curr_playlist->channels = attribs->channels;
1002
0
    } else {
1003
      /* Normal Playlist */
1004
0
      gf_assert((*playlist)->streams);
1005
0
      if (curr_playlist == NULL) {
1006
1007
        /* This is a "normal" playlist without any element in it */
1008
0
        PlaylistElement *subElement;
1009
0
        gf_assert(baseURL);
1010
0
        curr_playlist = playlist_element_new(TYPE_PLAYLIST, baseURL, attribs);
1011
0
        if (curr_playlist == NULL) {
1012
          /* out of memory */
1013
0
          gf_m3u8_master_playlist_del(playlist);
1014
0
          return GF_OUT_OF_MEM;
1015
0
        }
1016
0
        gf_assert(curr_playlist->element.playlist.elements);
1017
0
        gf_assert(curr_playlist->url && !curr_playlist->codecs);
1018
0
        curr_playlist->codecs = NULL;
1019
1020
0
        subElement = playlist_element_new(TYPE_UNKNOWN, fullURL, attribs);
1021
0
        if (attribs->init_url) {
1022
0
          gf_free(attribs->init_url);
1023
0
          attribs->init_url = NULL;
1024
0
        }
1025
0
        if (subElement == NULL) {
1026
0
          gf_m3u8_master_playlist_del(playlist);
1027
0
          playlist_element_del(curr_playlist);
1028
0
          return GF_OUT_OF_MEM;
1029
0
        }
1030
0
        gf_list_add(curr_playlist->element.playlist.elements, subElement);
1031
0
        gf_list_add(stream->variants, curr_playlist);
1032
0
        curr_playlist->element.playlist.computed_duration += subElement->duration_info;
1033
0
        gf_assert(stream);
1034
0
        gf_assert(stream->variants);
1035
0
        gf_assert(curr_playlist);
1036
0
      } else {
1037
0
        PlaylistElement *subElement = playlist_element_new(TYPE_UNKNOWN, fullURL, attribs);
1038
0
        if (curr_playlist->element_type != TYPE_PLAYLIST) {
1039
0
          curr_playlist->element_type = TYPE_PLAYLIST;
1040
0
          if (!curr_playlist->element.playlist.elements)
1041
0
            curr_playlist->element.playlist.elements = gf_list_new();
1042
0
        }
1043
0
        if (subElement == NULL) {
1044
0
          gf_m3u8_master_playlist_del(playlist);
1045
0
          playlist_element_del(curr_playlist);
1046
0
          return GF_OUT_OF_MEM;
1047
0
        }
1048
0
        gf_list_add(curr_playlist->element.playlist.elements, subElement);
1049
0
        curr_playlist->element.playlist.computed_duration += subElement->duration_info;
1050
0
      }
1051
0
    }
1052
1053
0
    curr_playlist->element.playlist.current_media_seq = attribs->current_media_seq;
1054
    /* We first set the default duration for element, aka targetDuration */
1055
0
    if (attribs->target_duration_in_seconds > 0) {
1056
0
      curr_playlist->element.playlist.target_duration = attribs->target_duration_in_seconds;
1057
0
      curr_playlist->duration_info = attribs->target_duration_in_seconds;
1058
0
    }
1059
0
    if (attribs->duration_in_seconds) {
1060
0
      if (curr_playlist->duration_info == 0) {
1061
        /* we set the playlist duration info as the duration of a segment, only if it's not set
1062
        There are cases of playlist with the last segment with a duration different from the others
1063
        (example: Apple bipbop test)*/
1064
0
        curr_playlist->duration_info = attribs->duration_in_seconds;
1065
0
      }
1066
0
    }
1067
0
    curr_playlist->element.playlist.media_seq_min = attribs->min_media_sequence;
1068
0
    curr_playlist->element.playlist.media_seq_max = attribs->current_media_seq;
1069
0
    curr_playlist->element.playlist.discontinuity = attribs->discontinuity;
1070
0
    if (attribs->bandwidth > 1)
1071
0
      curr_playlist->bandwidth = attribs->bandwidth;
1072
0
    if (attribs->is_playlist_ended)
1073
0
      curr_playlist->element.playlist.is_ended = GF_TRUE;
1074
0
  }
1075
  /* Cleanup all line-specific fields */
1076
0
  if (attribs->title) {
1077
0
    gf_free(attribs->title);
1078
0
    attribs->title = NULL;
1079
0
  }
1080
0
  attribs->duration_in_seconds = 0;
1081
0
  attribs->playlist_utc_timestamp = 0;
1082
0
  attribs->bandwidth = 0;
1083
0
  attribs->stream_id = 0;
1084
0
  attribs->is_default = 0;
1085
0
  attribs->is_autoselect = 0;
1086
0
  if (attribs->codecs != NULL) {
1087
0
    gf_free(attribs->codecs);
1088
0
    attribs->codecs = NULL;
1089
0
  }
1090
0
  if (attribs->language != NULL) {
1091
0
    gf_free(attribs->language);
1092
0
    attribs->language = NULL;
1093
0
  }
1094
0
  if (attribs->name != NULL) {
1095
0
    gf_free(attribs->name);
1096
0
    attribs->name = NULL;
1097
0
  }
1098
0
  if (attribs->group_audio != NULL) {
1099
0
    gf_free(attribs->group_audio);
1100
0
    attribs->group_audio = NULL;
1101
0
  }
1102
0
  if (attribs->group_video != NULL) {
1103
0
    gf_free(attribs->group_video);
1104
0
    attribs->group_video = NULL;
1105
0
  }
1106
0
  if (attribs->group_subtitle != NULL) {
1107
0
    gf_free(attribs->group_subtitle);
1108
0
    attribs->group_subtitle = NULL;
1109
0
  }
1110
0
  if (attribs->group_closed_captions != NULL) {
1111
0
    gf_free(attribs->group_closed_captions);
1112
0
    attribs->group_closed_captions = NULL;
1113
0
  }
1114
0
  return GF_OK;
1115
0
}
1116
1117
typedef struct
1118
{
1119
  char *name;
1120
  u64 start;
1121
  u32 size;
1122
  Double duration;
1123
} HLS_LLChunk;
1124
1125
static void reset_attribs(s_accumulated_attributes *attribs, Bool is_cleanup)
1126
0
{
1127
0
  attribs->width = attribs->height = 0;
1128
0
#define RST_ATTR(_name) if (attribs->_name) { gf_free(attribs->_name); attribs->_name = NULL; }
1129
1130
0
  RST_ATTR(codecs)
1131
0
  RST_ATTR(group_audio)
1132
0
  RST_ATTR(group_video)
1133
0
  RST_ATTR(group_subtitle)
1134
0
  RST_ATTR(group_closed_captions)
1135
0
  RST_ATTR(language)
1136
0
  RST_ATTR(title)
1137
0
  if (is_cleanup) {
1138
0
    RST_ATTR(key_url)
1139
0
    RST_ATTR(name)
1140
0
  }
1141
0
  RST_ATTR(init_url)
1142
0
  RST_ATTR(mediaURL)
1143
0
}
1144
1145
1146
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)
1147
0
{
1148
0
  int i, currentLineNumber;
1149
0
  FILE *f = NULL;
1150
0
  u8 *m3u8_payload;
1151
0
  u32 m3u8_size, m3u8pos;
1152
0
  char currentLine[M3U8_BUF_SIZE];
1153
0
  char **attributes = NULL;
1154
0
  Bool release_blob = GF_FALSE;
1155
0
  s_accumulated_attributes attribs;
1156
1157
0
  if (!strncmp(m3u8_file, "gmem://", 7)) {
1158
0
    GF_Err e = gf_blob_get(m3u8_file, &m3u8_payload,  &m3u8_size, NULL);
1159
0
    if (e) {
1160
0
      GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot Open m3u8 source %s for reading\n", m3u8_file));
1161
0
      return e;
1162
0
    }
1163
0
    release_blob = GF_TRUE;
1164
0
  } else {
1165
0
    f = gf_fopen(m3u8_file, "rt");
1166
0
    if (!f) {
1167
0
      GF_LOG(GF_LOG_ERROR, GF_LOG_DASH,("[M3U8] Cannot open m3u8 file %s for reading\n", m3u8_file));
1168
0
      return GF_URL_ERROR;
1169
0
    }
1170
0
  }
1171
1172
0
  memset(&attribs, 0, sizeof(s_accumulated_attributes));
1173
1174
0
#define _CLEANUP \
1175
0
  reset_attribs(&attribs, GF_TRUE);\
1176
0
  if (f) gf_fclose(f); \
1177
0
  else if (release_blob) gf_blob_release(m3u8_file);
1178
1179
1180
0
  if (*playlist == NULL) {
1181
0
    *playlist = master_playlist_new();
1182
0
    if (!(*playlist)) {
1183
0
      _CLEANUP
1184
0
      return GF_OUT_OF_MEM;
1185
0
    }
1186
0
  }
1187
0
  currentLineNumber = 0;
1188
0
  reset_attributes(&attribs);
1189
0
  m3u8pos = 0;
1190
0
  while (1) {
1191
0
    char *eof;
1192
0
    int len;
1193
0
    if (f) {
1194
0
      if (!gf_fgets(currentLine, sizeof(currentLine), f))
1195
0
        break;
1196
0
    } else {
1197
0
      u32 __idx = 0;
1198
0
      if (m3u8pos >= m3u8_size)
1199
0
        break;
1200
0
      while (1) {
1201
0
        if (__idx >= M3U8_BUF_SIZE) break;
1202
1203
0
        currentLine[__idx] = m3u8_payload[m3u8pos];
1204
0
        __idx++;
1205
0
        m3u8pos++;
1206
0
        if ((currentLine[__idx-1]=='\n') || (currentLine[__idx-1]=='\r') || (m3u8pos >= m3u8_size)) {
1207
0
          currentLine[__idx]=0;
1208
0
          break;
1209
0
        }
1210
0
      }
1211
0
    }
1212
0
    currentLineNumber++;
1213
0
    eof = strchr(currentLine, '\r');
1214
0
    if (eof)
1215
0
      eof[0] = '\0';
1216
0
    eof = strchr(currentLine, '\n');
1217
0
    if (eof)
1218
0
      eof[0] = '\0';
1219
0
    len = (u32) strlen(currentLine);
1220
0
    if (len < 1)
1221
0
      continue;
1222
0
    if (currentLineNumber == 1) {
1223
      /* Playlist MUST start with #EXTM3U */
1224
0
      if (len < 7 || (strncmp("#EXTM3U", currentLine, 7) != 0)) {
1225
0
        GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("Failed to parse M3U8 File, it should start with #EXTM3U, but was : %s\n", currentLine));
1226
0
        _CLEANUP
1227
0
        return GF_STREAM_NOT_FOUND;
1228
0
      }
1229
0
      continue;
1230
0
    }
1231
0
    if (currentLine[0] == '#') {
1232
      /* chunk */
1233
0
      if (!strncmp("#EXT-X-PART:", currentLine, 12)) {
1234
0
        GF_Err e = GF_NON_COMPLIANT_BITSTREAM;
1235
0
        char *sep;
1236
0
        char *file = strstr(currentLine, "URI=\"");
1237
0
        char *dur = strstr(currentLine, "DURATION=");
1238
0
        char *br = strstr(currentLine, "BYTERANGE=");
1239
1240
0
        if (strstr(currentLine, "INDEPENDENT=YES")) {
1241
0
          attribs.independent_part = GF_TRUE;
1242
0
        }
1243
0
        if (br) {
1244
0
          u64 start=0;
1245
0
          u32 size=0;
1246
0
          sep = strchr(br, ',');
1247
0
          if (sep) sep[0] = 0;
1248
0
          if (sscanf(br+10, "\"%u@"LLU"\"", &size, &start) != 2)
1249
0
            file = NULL;
1250
0
          attribs.byte_range_start = start;
1251
0
          attribs.byte_range_end = start + size - 1;
1252
0
        }
1253
0
        if (dur) {
1254
0
          sep = strchr(dur, ',');
1255
0
          if (sep) sep[0] = 0;
1256
0
          attribs.duration_in_seconds = atof(dur+9);
1257
0
        }
1258
1259
0
        if (file && dur) {
1260
0
          file += 5; // file starts with `URI:"`, move to start of URL
1261
          //find end quote
1262
0
          sep = strchr(file, '"');
1263
0
          if (!sep) {
1264
0
            e = GF_NON_COMPLIANT_BITSTREAM;
1265
0
            _CLEANUP
1266
0
            return e;
1267
0
          }
1268
0
          sep[0] = 0;
1269
1270
0
          attribs.low_latency = GF_TRUE;
1271
0
          attribs.is_media_segment = GF_TRUE;
1272
0
          e = declare_sub_playlist(file, baseURL, &attribs, sub_playlist, playlist, in_stream);
1273
1274
0
          (*playlist)->low_latency = GF_TRUE;
1275
0
          sep[0] = '"';
1276
0
        }
1277
0
        attribs.is_media_segment = GF_FALSE;
1278
0
        attribs.low_latency = GF_FALSE;
1279
0
        attribs.independent_part = GF_FALSE;
1280
0
        attribs.byte_range_start = attribs.byte_range_end = 0;
1281
0
        attribs.duration_in_seconds = 0;
1282
0
        if (e != GF_OK) {
1283
0
          _CLEANUP
1284
0
          return e;
1285
0
        }
1286
0
      }
1287
      /* A comment or a directive */
1288
0
      else if (!strncmp("#EXT", currentLine, 4)) {
1289
0
        attributes = parse_attributes(currentLine, &attribs);
1290
0
        if (attributes == NULL) {
1291
0
          GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8]Comment at line %d : %s\n", currentLineNumber, currentLine));
1292
0
        } else {
1293
0
          GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] Directive at line %d: \"%s\", attributes=", currentLineNumber, currentLine));
1294
0
          i = 0;
1295
0
          while (attributes[i] != NULL) {
1296
0
            GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, (" [%d]='%s'", i, attributes[i]));
1297
0
            gf_free(attributes[i]);
1298
0
            attributes[i] = NULL;
1299
0
            i++;
1300
0
          }
1301
0
          GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("\n"));
1302
0
          gf_free(attributes);
1303
0
          attributes = NULL;
1304
0
        }
1305
0
        if (attribs.is_playlist_ended) {
1306
0
          (*playlist)->playlist_needs_refresh = GF_FALSE;
1307
0
        }
1308
0
        if (attribs.independent_segments) {
1309
0
          (*playlist)->independent_segments = GF_TRUE;
1310
0
        }
1311
0
        if (attribs.low_latency) {
1312
0
          (*playlist)->low_latency = GF_TRUE;
1313
0
          attribs.low_latency = GF_FALSE;
1314
0
        }
1315
0
        if (attribs.version > (*playlist)->version) {
1316
0
          (*playlist)->version = attribs.version;
1317
0
        }
1318
0
        if (attribs.mediaURL) {
1319
0
          GF_Err e = declare_sub_playlist(attribs.mediaURL, baseURL, &attribs, sub_playlist, playlist, in_stream);
1320
0
          gf_free(attribs.mediaURL);
1321
0
          attribs.mediaURL = NULL;
1322
0
          if (e != GF_OK) {
1323
0
            _CLEANUP
1324
0
            return e;
1325
0
          }
1326
0
        }
1327
0
      }
1328
0
    } else {
1329
1330
      /*file encountered: sub-playlist or segment*/
1331
0
      GF_Err e;
1332
0
      const char *pl_url = baseURL;
1333
      //this is the first (master) playlist but what is parsed is not a master playlist
1334
      //we will create a master + child with xlink, but the xlink must be only the file name
1335
      //otherwise dir/pl_video.m3u8 will be translated in master(dir/pl_video.m3u8) [ child(xlink=dir/pl_video.m3u8) ]
1336
      //thus xlink resolution would give dir/dir/pl_video.m3u8
1337
0
      if (is_master && !attribs.is_master_playlist)
1338
0
        pl_url = gf_file_basename(baseURL);
1339
0
      e = declare_sub_playlist(currentLine, pl_url, &attribs, sub_playlist, playlist, in_stream);
1340
0
      attribs.current_media_seq += 1;
1341
0
      if (e != GF_OK) {
1342
0
        _CLEANUP
1343
0
        return e;
1344
0
      }
1345
1346
      //do not reset all attributes but at least set width/height/codecs to NULL, otherwise we may miss detection
1347
      //of audio-only playlists in av sequences
1348
1349
0
      reset_attribs(&attribs, GF_FALSE);
1350
0
    }
1351
0
  }
1352
1353
0
  _CLEANUP
1354
1355
0
#undef _CLEANUP
1356
1357
0
  for (i=0; i<(int)gf_list_count((*playlist)->streams); i++) {
1358
0
    u32 j;
1359
0
    Stream *prog = gf_list_get((*playlist)->streams, i);
1360
0
    prog->computed_duration = 0;
1361
0
    for (j=0; j<gf_list_count(prog->variants); j++) {
1362
0
      PlaylistElement *ple = gf_list_get(prog->variants, j);
1363
0
      if (ple->element_type == TYPE_PLAYLIST) {
1364
0
        if (ple->element.playlist.computed_duration > prog->computed_duration)
1365
0
          prog->computed_duration = ple->element.playlist.computed_duration;
1366
0
      }
1367
0
    }
1368
1369
0
  }
1370
1371
0
  if (attribs.version < attribs.compatibility_version) {
1372
0
    GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[M3U8] Version %d specified but tags from version %d detected\n", attribs.version, attribs.compatibility_version));
1373
0
  }
1374
0
  return GF_OK;
1375
0
}