|
|
|
@@ -1,6 +1,7 @@ |
|
|
|
/* |
|
|
|
* Apple HTTP Live Streaming demuxer |
|
|
|
* Copyright (c) 2010 Martin Storsjo |
|
|
|
* Copyright (c) 2013 Anssi Hannula |
|
|
|
* |
|
|
|
* This file is part of FFmpeg. |
|
|
|
* |
|
|
|
@@ -38,6 +39,9 @@ |
|
|
|
|
|
|
|
#define INITIAL_BUFFER_SIZE 32768 |
|
|
|
|
|
|
|
#define MAX_FIELD_LEN 64 |
|
|
|
#define MAX_CHARACTERISTICS_LEN 512 |
|
|
|
|
|
|
|
/* |
|
|
|
* An apple http stream consists of a playlist with media segment files, |
|
|
|
* played sequentially. There may be several playlists with the same |
|
|
|
@@ -63,6 +67,8 @@ struct segment { |
|
|
|
uint8_t iv[16]; |
|
|
|
}; |
|
|
|
|
|
|
|
struct rendition; |
|
|
|
|
|
|
|
/* |
|
|
|
* Each playlist has its own demuxer. If it currently is active, |
|
|
|
* it has an open AVIOContext too, and potentially an AVPacket |
|
|
|
@@ -90,12 +96,40 @@ struct playlist { |
|
|
|
|
|
|
|
char key_url[MAX_URL_SIZE]; |
|
|
|
uint8_t key[16]; |
|
|
|
|
|
|
|
/* Renditions associated with this playlist, if any. |
|
|
|
* Alternative rendition playlists have a single rendition associated |
|
|
|
* with them, and variant main Media Playlists may have |
|
|
|
* multiple (playlist-less) renditions associated with them. */ |
|
|
|
int n_renditions; |
|
|
|
struct rendition **renditions; |
|
|
|
}; |
|
|
|
|
|
|
|
/* |
|
|
|
* Renditions are e.g. alternative subtitle or audio streams. |
|
|
|
* The rendition may either be an external playlist or it may be |
|
|
|
* contained in the main Media Playlist of the variant (in which case |
|
|
|
* playlist is NULL). |
|
|
|
*/ |
|
|
|
struct rendition { |
|
|
|
enum AVMediaType type; |
|
|
|
struct playlist *playlist; |
|
|
|
char group_id[MAX_FIELD_LEN]; |
|
|
|
char language[MAX_FIELD_LEN]; |
|
|
|
char name[MAX_FIELD_LEN]; |
|
|
|
int disposition; |
|
|
|
}; |
|
|
|
|
|
|
|
struct variant { |
|
|
|
int bandwidth; |
|
|
|
|
|
|
|
/* every variant contains at least the main Media Playlist in index 0 */ |
|
|
|
int n_playlists; |
|
|
|
struct playlist **playlists; |
|
|
|
|
|
|
|
char audio_group[MAX_FIELD_LEN]; |
|
|
|
char video_group[MAX_FIELD_LEN]; |
|
|
|
char subtitles_group[MAX_FIELD_LEN]; |
|
|
|
}; |
|
|
|
|
|
|
|
typedef struct HLSContext { |
|
|
|
@@ -103,6 +137,8 @@ typedef struct HLSContext { |
|
|
|
struct variant **variants; |
|
|
|
int n_playlists; |
|
|
|
struct playlist **playlists; |
|
|
|
int n_renditions; |
|
|
|
struct rendition **renditions; |
|
|
|
|
|
|
|
int cur_seq_no; |
|
|
|
int end_of_segment; |
|
|
|
@@ -139,6 +175,7 @@ static void free_playlist_list(HLSContext *c) |
|
|
|
for (i = 0; i < c->n_playlists; i++) { |
|
|
|
struct playlist *pls = c->playlists[i]; |
|
|
|
free_segment_list(pls); |
|
|
|
av_freep(&pls->renditions); |
|
|
|
av_free_packet(&pls->pkt); |
|
|
|
av_free(pls->pb.buffer); |
|
|
|
if (pls->input) |
|
|
|
@@ -167,6 +204,15 @@ static void free_variant_list(HLSContext *c) |
|
|
|
c->n_variants = 0; |
|
|
|
} |
|
|
|
|
|
|
|
static void free_rendition_list(HLSContext *c) |
|
|
|
{ |
|
|
|
int i; |
|
|
|
for (i = 0; i < c->n_renditions; i++) |
|
|
|
av_free(c->renditions[i]); |
|
|
|
av_freep(&c->renditions); |
|
|
|
c->n_renditions = 0; |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* Used to reset a statically allocated AVPacket to a clean slate, |
|
|
|
* containing no data. |
|
|
|
@@ -189,7 +235,15 @@ static struct playlist *new_playlist(HLSContext *c, const char *url, |
|
|
|
return pls; |
|
|
|
} |
|
|
|
|
|
|
|
static struct variant *new_variant(HLSContext *c, int bandwidth, |
|
|
|
struct variant_info { |
|
|
|
char bandwidth[20]; |
|
|
|
/* variant group ids: */ |
|
|
|
char audio[MAX_FIELD_LEN]; |
|
|
|
char video[MAX_FIELD_LEN]; |
|
|
|
char subtitles[MAX_FIELD_LEN]; |
|
|
|
}; |
|
|
|
|
|
|
|
static struct variant *new_variant(HLSContext *c, struct variant_info *info, |
|
|
|
const char *url, const char *base) |
|
|
|
{ |
|
|
|
struct variant *var; |
|
|
|
@@ -203,22 +257,33 @@ static struct variant *new_variant(HLSContext *c, int bandwidth, |
|
|
|
if (!var) |
|
|
|
return NULL; |
|
|
|
|
|
|
|
var->bandwidth = bandwidth; |
|
|
|
if (info) { |
|
|
|
var->bandwidth = atoi(info->bandwidth); |
|
|
|
strcpy(var->audio_group, info->audio); |
|
|
|
strcpy(var->video_group, info->video); |
|
|
|
strcpy(var->subtitles_group, info->subtitles); |
|
|
|
} |
|
|
|
|
|
|
|
dynarray_add(&c->variants, &c->n_variants, var); |
|
|
|
dynarray_add(&var->playlists, &var->n_playlists, pls); |
|
|
|
return var; |
|
|
|
} |
|
|
|
|
|
|
|
struct variant_info { |
|
|
|
char bandwidth[20]; |
|
|
|
}; |
|
|
|
|
|
|
|
static void handle_variant_args(struct variant_info *info, const char *key, |
|
|
|
int key_len, char **dest, int *dest_len) |
|
|
|
{ |
|
|
|
if (!strncmp(key, "BANDWIDTH=", key_len)) { |
|
|
|
*dest = info->bandwidth; |
|
|
|
*dest_len = sizeof(info->bandwidth); |
|
|
|
} else if (!strncmp(key, "AUDIO=", key_len)) { |
|
|
|
*dest = info->audio; |
|
|
|
*dest_len = sizeof(info->audio); |
|
|
|
} else if (!strncmp(key, "VIDEO=", key_len)) { |
|
|
|
*dest = info->video; |
|
|
|
*dest_len = sizeof(info->video); |
|
|
|
} else if (!strncmp(key, "SUBTITLES=", key_len)) { |
|
|
|
*dest = info->subtitles; |
|
|
|
*dest_len = sizeof(info->subtitles); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@@ -243,10 +308,137 @@ static void handle_key_args(struct key_info *info, const char *key, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
struct rendition_info { |
|
|
|
char type[16]; |
|
|
|
char uri[MAX_URL_SIZE]; |
|
|
|
char group_id[MAX_FIELD_LEN]; |
|
|
|
char language[MAX_FIELD_LEN]; |
|
|
|
char assoc_language[MAX_FIELD_LEN]; |
|
|
|
char name[MAX_FIELD_LEN]; |
|
|
|
char defaultr[4]; |
|
|
|
char forced[4]; |
|
|
|
char characteristics[MAX_CHARACTERISTICS_LEN]; |
|
|
|
}; |
|
|
|
|
|
|
|
static struct rendition *new_rendition(HLSContext *c, struct rendition_info *info, |
|
|
|
const char *url_base) |
|
|
|
{ |
|
|
|
struct rendition *rend; |
|
|
|
enum AVMediaType type = AVMEDIA_TYPE_UNKNOWN; |
|
|
|
char *characteristic; |
|
|
|
char *chr_ptr; |
|
|
|
char *saveptr; |
|
|
|
|
|
|
|
if (!strcmp(info->type, "AUDIO")) |
|
|
|
type = AVMEDIA_TYPE_AUDIO; |
|
|
|
else if (!strcmp(info->type, "VIDEO")) |
|
|
|
type = AVMEDIA_TYPE_VIDEO; |
|
|
|
else if (!strcmp(info->type, "SUBTITLES")) |
|
|
|
type = AVMEDIA_TYPE_SUBTITLE; |
|
|
|
else if (!strcmp(info->type, "CLOSED-CAPTIONS")) |
|
|
|
/* CLOSED-CAPTIONS is ignored since we do not support CEA-608 CC in |
|
|
|
* AVC SEI RBSP anyway */ |
|
|
|
return NULL; |
|
|
|
|
|
|
|
if (type == AVMEDIA_TYPE_UNKNOWN) |
|
|
|
return NULL; |
|
|
|
|
|
|
|
/* URI is mandatory for subtitles as per spec */ |
|
|
|
if (type == AVMEDIA_TYPE_SUBTITLE && !info->uri[0]) |
|
|
|
return NULL; |
|
|
|
|
|
|
|
/* TODO: handle subtitles (each segment has to parsed separately) */ |
|
|
|
if (type == AVMEDIA_TYPE_SUBTITLE) |
|
|
|
return NULL; |
|
|
|
|
|
|
|
rend = av_mallocz(sizeof(struct rendition)); |
|
|
|
if (!rend) |
|
|
|
return NULL; |
|
|
|
|
|
|
|
dynarray_add(&c->renditions, &c->n_renditions, rend); |
|
|
|
|
|
|
|
rend->type = type; |
|
|
|
strcpy(rend->group_id, info->group_id); |
|
|
|
strcpy(rend->language, info->language); |
|
|
|
strcpy(rend->name, info->name); |
|
|
|
|
|
|
|
/* add the playlist if this is an external rendition */ |
|
|
|
if (info->uri[0]) { |
|
|
|
rend->playlist = new_playlist(c, info->uri, url_base); |
|
|
|
if (rend->playlist) |
|
|
|
dynarray_add(&rend->playlist->renditions, |
|
|
|
&rend->playlist->n_renditions, rend); |
|
|
|
} |
|
|
|
|
|
|
|
if (info->assoc_language[0]) { |
|
|
|
int langlen = strlen(rend->language); |
|
|
|
if (langlen < sizeof(rend->language) - 3) { |
|
|
|
rend->language[langlen] = ','; |
|
|
|
strncpy(rend->language + langlen + 1, info->assoc_language, |
|
|
|
sizeof(rend->language) - langlen - 2); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!strcmp(info->defaultr, "YES")) |
|
|
|
rend->disposition |= AV_DISPOSITION_DEFAULT; |
|
|
|
if (!strcmp(info->forced, "YES")) |
|
|
|
rend->disposition |= AV_DISPOSITION_FORCED; |
|
|
|
|
|
|
|
chr_ptr = info->characteristics; |
|
|
|
while ((characteristic = av_strtok(chr_ptr, ",", &saveptr))) { |
|
|
|
if (!strcmp(characteristic, "public.accessibility.describes-music-and-sound")) |
|
|
|
rend->disposition |= AV_DISPOSITION_HEARING_IMPAIRED; |
|
|
|
else if (!strcmp(characteristic, "public.accessibility.describes-video")) |
|
|
|
rend->disposition |= AV_DISPOSITION_VISUAL_IMPAIRED; |
|
|
|
|
|
|
|
chr_ptr = NULL; |
|
|
|
} |
|
|
|
|
|
|
|
return rend; |
|
|
|
} |
|
|
|
|
|
|
|
static void handle_rendition_args(struct rendition_info *info, const char *key, |
|
|
|
int key_len, char **dest, int *dest_len) |
|
|
|
{ |
|
|
|
if (!strncmp(key, "TYPE=", key_len)) { |
|
|
|
*dest = info->type; |
|
|
|
*dest_len = sizeof(info->type); |
|
|
|
} else if (!strncmp(key, "URI=", key_len)) { |
|
|
|
*dest = info->uri; |
|
|
|
*dest_len = sizeof(info->uri); |
|
|
|
} else if (!strncmp(key, "GROUP-ID=", key_len)) { |
|
|
|
*dest = info->group_id; |
|
|
|
*dest_len = sizeof(info->group_id); |
|
|
|
} else if (!strncmp(key, "LANGUAGE=", key_len)) { |
|
|
|
*dest = info->language; |
|
|
|
*dest_len = sizeof(info->language); |
|
|
|
} else if (!strncmp(key, "ASSOC-LANGUAGE=", key_len)) { |
|
|
|
*dest = info->assoc_language; |
|
|
|
*dest_len = sizeof(info->assoc_language); |
|
|
|
} else if (!strncmp(key, "NAME=", key_len)) { |
|
|
|
*dest = info->name; |
|
|
|
*dest_len = sizeof(info->name); |
|
|
|
} else if (!strncmp(key, "DEFAULT=", key_len)) { |
|
|
|
*dest = info->defaultr; |
|
|
|
*dest_len = sizeof(info->defaultr); |
|
|
|
} else if (!strncmp(key, "FORCED=", key_len)) { |
|
|
|
*dest = info->forced; |
|
|
|
*dest_len = sizeof(info->forced); |
|
|
|
} else if (!strncmp(key, "CHARACTERISTICS=", key_len)) { |
|
|
|
*dest = info->characteristics; |
|
|
|
*dest_len = sizeof(info->characteristics); |
|
|
|
} |
|
|
|
/* |
|
|
|
* ignored: |
|
|
|
* - AUTOSELECT: client may autoselect based on e.g. system language |
|
|
|
* - INSTREAM-ID: EIA-608 closed caption number ("CC1".."CC4") |
|
|
|
*/ |
|
|
|
} |
|
|
|
|
|
|
|
static int parse_playlist(HLSContext *c, const char *url, |
|
|
|
struct playlist *pls, AVIOContext *in) |
|
|
|
{ |
|
|
|
int ret = 0, is_segment = 0, is_variant = 0, bandwidth = 0; |
|
|
|
int ret = 0, is_segment = 0, is_variant = 0; |
|
|
|
int64_t duration = 0; |
|
|
|
enum KeyType key_type = KEY_NONE; |
|
|
|
uint8_t iv[16] = ""; |
|
|
|
@@ -256,6 +448,7 @@ static int parse_playlist(HLSContext *c, const char *url, |
|
|
|
const char *ptr; |
|
|
|
int close_in = 0; |
|
|
|
uint8_t *new_url = NULL; |
|
|
|
struct variant_info variant_info; |
|
|
|
|
|
|
|
if (!in) { |
|
|
|
AVDictionary *opts = NULL; |
|
|
|
@@ -291,11 +484,10 @@ static int parse_playlist(HLSContext *c, const char *url, |
|
|
|
while (!url_feof(in)) { |
|
|
|
read_chomp_line(in, line, sizeof(line)); |
|
|
|
if (av_strstart(line, "#EXT-X-STREAM-INF:", &ptr)) { |
|
|
|
struct variant_info info = {{0}}; |
|
|
|
is_variant = 1; |
|
|
|
memset(&variant_info, 0, sizeof(variant_info)); |
|
|
|
ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_variant_args, |
|
|
|
&info); |
|
|
|
bandwidth = atoi(info.bandwidth); |
|
|
|
&variant_info); |
|
|
|
} else if (av_strstart(line, "#EXT-X-KEY:", &ptr)) { |
|
|
|
struct key_info info = {{0}}; |
|
|
|
ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_key_args, |
|
|
|
@@ -309,9 +501,14 @@ static int parse_playlist(HLSContext *c, const char *url, |
|
|
|
has_iv = 1; |
|
|
|
} |
|
|
|
av_strlcpy(key, info.uri, sizeof(key)); |
|
|
|
} else if (av_strstart(line, "#EXT-X-MEDIA:", &ptr)) { |
|
|
|
struct rendition_info info = {{0}}; |
|
|
|
ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_rendition_args, |
|
|
|
&info); |
|
|
|
new_rendition(c, &info, url); |
|
|
|
} else if (av_strstart(line, "#EXT-X-TARGETDURATION:", &ptr)) { |
|
|
|
if (!pls) { |
|
|
|
if (!new_variant(c, 0, url, NULL)) { |
|
|
|
if (!new_variant(c, NULL, url, NULL)) { |
|
|
|
ret = AVERROR(ENOMEM); |
|
|
|
goto fail; |
|
|
|
} |
|
|
|
@@ -320,7 +517,7 @@ static int parse_playlist(HLSContext *c, const char *url, |
|
|
|
pls->target_duration = atoi(ptr) * AV_TIME_BASE; |
|
|
|
} else if (av_strstart(line, "#EXT-X-MEDIA-SEQUENCE:", &ptr)) { |
|
|
|
if (!pls) { |
|
|
|
if (!new_variant(c, 0, url, NULL)) { |
|
|
|
if (!new_variant(c, NULL, url, NULL)) { |
|
|
|
ret = AVERROR(ENOMEM); |
|
|
|
goto fail; |
|
|
|
} |
|
|
|
@@ -337,12 +534,11 @@ static int parse_playlist(HLSContext *c, const char *url, |
|
|
|
continue; |
|
|
|
} else if (line[0]) { |
|
|
|
if (is_variant) { |
|
|
|
if (!new_variant(c, bandwidth, line, url)) { |
|
|
|
if (!new_variant(c, &variant_info, line, url)) { |
|
|
|
ret = AVERROR(ENOMEM); |
|
|
|
goto fail; |
|
|
|
} |
|
|
|
is_variant = 0; |
|
|
|
bandwidth = 0; |
|
|
|
} |
|
|
|
if (is_segment) { |
|
|
|
struct segment *seg; |
|
|
|
@@ -543,6 +739,60 @@ static int playlist_in_multiple_variants(HLSContext *c, struct playlist *pls) |
|
|
|
return variant_count >= 2; |
|
|
|
} |
|
|
|
|
|
|
|
static void add_renditions_to_variant(HLSContext *c, struct variant *var, |
|
|
|
enum AVMediaType type, const char *group_id) |
|
|
|
{ |
|
|
|
int i; |
|
|
|
|
|
|
|
for (i = 0; i < c->n_renditions; i++) { |
|
|
|
struct rendition *rend = c->renditions[i]; |
|
|
|
|
|
|
|
if (rend->type == type && !strcmp(rend->group_id, group_id)) { |
|
|
|
|
|
|
|
if (rend->playlist) |
|
|
|
/* rendition is an external playlist |
|
|
|
* => add the playlist to the variant */ |
|
|
|
dynarray_add(&var->playlists, &var->n_playlists, rend->playlist); |
|
|
|
else |
|
|
|
/* rendition is part of the variant main Media Playlist |
|
|
|
* => add the rendition to the main Media Playlist */ |
|
|
|
dynarray_add(&var->playlists[0]->renditions, |
|
|
|
&var->playlists[0]->n_renditions, |
|
|
|
rend); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
static void add_metadata_from_renditions(AVFormatContext *s, struct playlist *pls, |
|
|
|
enum AVMediaType type) |
|
|
|
{ |
|
|
|
int rend_idx = 0; |
|
|
|
int i; |
|
|
|
|
|
|
|
for (i = 0; i < pls->ctx->nb_streams; i++) { |
|
|
|
AVStream *st = s->streams[pls->stream_offset + i]; |
|
|
|
|
|
|
|
if (st->codec->codec_type != type) |
|
|
|
continue; |
|
|
|
|
|
|
|
for (; rend_idx < pls->n_renditions; rend_idx++) { |
|
|
|
struct rendition *rend = pls->renditions[rend_idx]; |
|
|
|
|
|
|
|
if (rend->type != type) |
|
|
|
continue; |
|
|
|
|
|
|
|
if (rend->language[0]) |
|
|
|
av_dict_set(&st->metadata, "language", rend->language, 0); |
|
|
|
if (rend->name[0]) |
|
|
|
av_dict_set(&st->metadata, "comment", rend->name, 0); |
|
|
|
|
|
|
|
st->disposition |= rend->disposition; |
|
|
|
} |
|
|
|
if (rend_idx >=pls->n_renditions) |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
static int hls_read_header(AVFormatContext *s) |
|
|
|
{ |
|
|
|
URLContext *u = (s->flags & AVFMT_FLAG_CUSTOM_IO) ? NULL : s->pb->opaque; |
|
|
|
@@ -605,6 +855,18 @@ static int hls_read_header(AVFormatContext *s) |
|
|
|
s->duration = duration; |
|
|
|
} |
|
|
|
|
|
|
|
/* Associate renditions with variants */ |
|
|
|
for (i = 0; i < c->n_variants; i++) { |
|
|
|
struct variant *var = c->variants[i]; |
|
|
|
|
|
|
|
if (var->audio_group[0]) |
|
|
|
add_renditions_to_variant(c, var, AVMEDIA_TYPE_AUDIO, var->audio_group); |
|
|
|
if (var->video_group[0]) |
|
|
|
add_renditions_to_variant(c, var, AVMEDIA_TYPE_VIDEO, var->video_group); |
|
|
|
if (var->subtitles_group[0]) |
|
|
|
add_renditions_to_variant(c, var, AVMEDIA_TYPE_SUBTITLE, var->subtitles_group); |
|
|
|
} |
|
|
|
|
|
|
|
/* Open the demuxer for each playlist */ |
|
|
|
for (i = 0; i < c->n_playlists; i++) { |
|
|
|
struct playlist *pls = c->playlists[i]; |
|
|
|
@@ -668,6 +930,10 @@ static int hls_read_header(AVFormatContext *s) |
|
|
|
avcodec_copy_context(st->codec, pls->ctx->streams[j]->codec); |
|
|
|
} |
|
|
|
|
|
|
|
add_metadata_from_renditions(s, pls, AVMEDIA_TYPE_AUDIO); |
|
|
|
add_metadata_from_renditions(s, pls, AVMEDIA_TYPE_VIDEO); |
|
|
|
add_metadata_from_renditions(s, pls, AVMEDIA_TYPE_SUBTITLE); |
|
|
|
|
|
|
|
stream_offset += pls->ctx->nb_streams; |
|
|
|
} |
|
|
|
|
|
|
|
@@ -709,6 +975,7 @@ static int hls_read_header(AVFormatContext *s) |
|
|
|
fail: |
|
|
|
free_playlist_list(c); |
|
|
|
free_variant_list(c); |
|
|
|
free_rendition_list(c); |
|
|
|
return ret; |
|
|
|
} |
|
|
|
|
|
|
|
@@ -850,6 +1117,7 @@ static int hls_close(AVFormatContext *s) |
|
|
|
|
|
|
|
free_playlist_list(c); |
|
|
|
free_variant_list(c); |
|
|
|
free_rendition_list(c); |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
|