/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 | } |