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