@@ -321,6 +321,9 @@ available in a metadata key named "variant_bitrate".
It accepts the following options:
@table @option
+@item abr
+enable abr to switch streams.
+
@item live_start_index
segment index to start live streams at (negative values are from the end).
@@ -189,6 +189,15 @@ struct variant {
char subtitles_group[MAX_FIELD_LEN];
};
+struct throughput {
+ int n_throughputs;
+
+ /* throughputs are in kbps, always record the last 20 times */
+ float throughput_fifo[20];
+ int head;
+ int tail;
+};
+
typedef struct HLSContext {
AVClass *class;
AVFormatContext *ctx;
@@ -213,8 +222,36 @@ typedef struct HLSContext {
int http_multiple;
int http_seekable;
AVIOContext *playlist_pb;
+
+ int abr;
+ struct throughput *throughputs;
+ int can_switch;
+ int switch_request2;
+ int switch_delay;
+ int64_t switch_timestamp;
+ int64_t delta_timestamp;
+ int cur_pls;
} HLSContext;
+static struct segment *next_segment(struct playlist *pls);
+static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg, AVIOContext **in);
+
+static void sync_cur_seq(HLSContext *c) {
+ int i;
+ for (i = 0; i < c->n_playlists; i++) {
+ struct playlist *pls = c->playlists[i];
+ pls->cur_seq_no = c->cur_seq_no;
+ }
+}
+
+static struct segment *next2_segment(struct playlist *pls)
+{
+ int n = pls->cur_seq_no - pls->start_seq_no + 2;
+ if (n >= pls->n_segments)
+ return NULL;
+ return pls->segments[n];
+}
+
static void free_segment_dynarray(struct segment **segments, int n_segments)
{
int i;
@@ -624,6 +661,33 @@ static int open_url_keepalive(AVFormatContext *s, AVIOContext **pb,
#endif
}
+static int update_throughputs(struct throughput *thr, float time, int pb_size)
+{
+ if (pb_size <= 0 || time <= 0)
+ return -1;
+ if (thr->n_throughputs < 20) {
+ ++thr->n_throughputs;
+ thr->throughput_fifo[thr->tail] = (float)(pb_size) / time;
+ } else {
+ thr->throughput_fifo[thr->tail] = (float)(pb_size) / time;
+ ++thr->head;
+ }
+
+ thr->tail = (thr->tail + 1) % 20;
+ return 0;
+}
+
+static int64_t get_switch_timestamp(HLSContext *c, struct playlist *pls)
+{
+ int64_t pos = c->first_timestamp == AV_NOPTS_VALUE ?
+ 0 : c->first_timestamp;
+
+ for (int i = 0; i < pls->cur_seq_no + 2; i++) {
+ pos += pls->segments[i]->duration;
+ }
+ return pos;
+}
+
static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
AVDictionary *opts, AVDictionary *opts2, int *is_http_out)
{
@@ -633,7 +697,10 @@ static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
int ret;
int is_http = 0;
- if (av_strstart(url, "crypto", NULL)) {
+ if (av_strstart(url, "abr", NULL)) {
+ if (url[3] == '+' || url[3] == ':')
+ proto_name = avio_find_protocol_name(url + 4);
+ } else if (av_strstart(url, "crypto", NULL)) {
if (url[6] == '+' || url[6] == ':')
proto_name = avio_find_protocol_name(url + 7);
} else if (av_strstart(url, "data", NULL)) {
@@ -665,6 +732,8 @@ static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
if (!strncmp(proto_name, url, strlen(proto_name)) && url[strlen(proto_name)] == ':')
;
+ else if (av_strstart(url, "abr", NULL) && !strncmp(proto_name, url + 4, strlen(proto_name)) && url[4 + strlen(proto_name)] == ':')
+ ;
else if (av_strstart(url, "crypto", NULL) && !strncmp(proto_name, url + 7, strlen(proto_name)) && url[7 + strlen(proto_name)] == ':')
;
else if (av_strstart(url, "data", NULL) && !strncmp(proto_name, url + 5, strlen(proto_name)) && url[5 + strlen(proto_name)] == ':')
@@ -690,6 +759,31 @@ static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
} else {
ret = s->io_open(s, pb, url, AVIO_FLAG_READ, &tmp);
}
+ if (c->abr) {
+ AVDictionary *abr_ret = NULL;
+ AVDictionaryEntry *en = NULL;
+ struct segment *seg;
+ int pb_size, switch_request;
+ av_opt_get_dict_val(*pb, "abr-metadata", AV_OPT_SEARCH_CHILDREN, &abr_ret);
+ en = av_dict_get(abr_ret, "download_time", NULL, 0);
+ pb_size = avio_size(*pb);
+ update_throughputs(c->throughputs, atoi(en->value) / 1000.0, pb_size);
+ av_log(s, AV_LOG_VERBOSE, "[abr] time=%.2fms, size=%.2fkb\n", atoi(en->value) / 1000.0, pb_size / 1000.0);
+ en = av_dict_get(abr_ret, "switch_request", NULL, 0);
+ switch_request = atoi(en->value);
+ av_log(s, AV_LOG_VERBOSE, "[abr] switch request: %s\n", en->value);
+ if (switch_request != -1) {
+ c->switch_request2 = switch_request;
+ c->switch_delay = 2;
+ c->can_switch = 0;
+ sync_cur_seq(c);
+ seg = next2_segment(c->playlists[c->switch_request2]);
+ c->switch_timestamp = get_switch_timestamp(c, c->playlists[c->switch_request2]);
+ av_log(s, AV_LOG_VERBOSE, "[abr] switch timestamp: %ld\n", c->switch_timestamp);
+ c->playlists[c->switch_request2]->input_next_requested = 1;
+ ret = open_input(c, c->playlists[c->switch_request2], seg, &c->playlists[c->switch_request2]->input_next);
+ }
+ }
if (ret >= 0) {
// update cookies on http response with setcookies.
char *new_cookies = NULL;
@@ -1219,6 +1313,33 @@ static void intercept_id3(struct playlist *pls, uint8_t *buf,
pls->is_id3_timestamped = (pls->id3_mpegts_timestamp != AV_NOPTS_VALUE);
}
+static int abrinfo_to_dict(HLSContext *c, char **abr_info)
+{
+ struct throughput *thr = c->throughputs;
+ char buffer[MAX_URL_SIZE];
+ int size, i;
+ size = snprintf(buffer, sizeof(buffer), "format=hls:");
+ size += snprintf(buffer + size, sizeof(buffer) - size, "cur_pls=%d:", c->cur_pls);
+ size += snprintf(buffer + size, sizeof(buffer) - size, "can_switch=%d:", c->can_switch);
+ size += snprintf(buffer + size, sizeof(buffer) - size, "n_variants=%d:", c->n_variants);
+ for (i = 0; i < c->n_variants; i++) {
+ struct variant *v = c->variants[i];
+ size += snprintf(buffer + size, sizeof(buffer) - size, "variant_bitrate%d=%d:", i, v->bandwidth);
+ }
+ size += snprintf(buffer + size, sizeof(buffer) - size, "n_throughputs=%d:", thr->n_throughputs);
+ if (thr->n_throughputs > 0) {
+ i = thr->head;
+ do {
+ size += snprintf(buffer + size, sizeof(buffer) - size, "throughputs%d=%.2f:", i, thr->throughput_fifo[i]);
+ i = (i + 1) % 20;
+ } while (i != thr->tail);
+ }
+ *abr_info = av_malloc(size);
+ snprintf(*abr_info, size, "%s", buffer);
+ av_log(c, AV_LOG_DEBUG, "[abr] abr_info: %s\n", *abr_info);
+ return 0;
+}
+
static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg, AVIOContext **in)
{
AVDictionary *opts = NULL;
@@ -1238,8 +1359,20 @@ static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg,
av_log(pls->parent, AV_LOG_VERBOSE, "HLS request for url '%s', offset %"PRId64", playlist %d\n",
seg->url, seg->url_offset, pls->index);
+ if (c->abr) {
+ char *abr_opts;
+ abrinfo_to_dict(c, &abr_opts);
+ av_dict_set(&opts, "abr-params", abr_opts, 0); // how to setup flag?
+ av_free(abr_opts);
+ }
if (seg->key_type == KEY_NONE) {
- ret = open_url(pls->parent, in, seg->url, c->avio_opts, opts, &is_http);
+ char url_abr[MAX_URL_SIZE];
+ if (c->abr) {
+ snprintf(url_abr, sizeof(url_abr), "abr:%s", seg->url); // + or : tbd
+ ret = open_url(pls->parent, in, url_abr, c->avio_opts, opts, &is_http);
+ } else {
+ ret = open_url(pls->parent, in, seg->url, c->avio_opts, opts, &is_http);
+ }
} else if (seg->key_type == KEY_AES_128) {
char iv[33], key[33], url[MAX_URL_SIZE];
if (strcmp(seg->key, pls->key_url)) {
@@ -1273,6 +1406,7 @@ static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg,
goto cleanup;
}
ret = 0;
+ // TODO: Add abr prefix for crypto
} else if (seg->key_type == KEY_SAMPLE_AES) {
av_log(pls->parent, AV_LOG_ERROR,
"SAMPLE-AES encryption is not supported yet\n");
@@ -1435,7 +1569,17 @@ restart:
/* Check that the playlist is still needed before opening a new
* segment. */
v->needed = playlist_needed(v);
-
+ if (c->abr) {
+ if (c->switch_request2 == -1)
+ ;
+ else if (v->needed && c->switch_request2 != v->index && c->switch_delay <= 0) {
+ av_log(v->parent, AV_LOG_VERBOSE, "read_data: needed but not download playlist %d ('%s')\n",
+ v->index, v->url);
+ return AVERROR_EOF;
+ }
+ if (!c->can_switch)
+ c->can_switch = 1;
+ }
if (!v->needed) {
av_log(v->parent, AV_LOG_INFO, "No longer receiving playlist %d ('%s')\n",
v->index, v->url);
@@ -1530,7 +1674,12 @@ reload:
}
seg = next_segment(v);
- if (c->http_multiple == 1 && !v->input_next_requested &&
+
+ // when get switch_request, old stream should stop downloading next seg
+ if (c->abr && c->switch_request2 != v->index && c->switch_timestamp != AV_NOPTS_VALUE
+ && ((c->cur_timestamp + seg->duration * 2) >= c->switch_timestamp) )
+ ;
+ else if (c->http_multiple == 1 && !v->input_next_requested &&
seg && seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL)) {
ret = open_input(c, v, seg, &v->input_next);
if (ret < 0) {
@@ -1563,11 +1712,17 @@ reload:
return ret;
}
- if (c->http_persistent &&
+ if (c->http_persistent && !c->abr &&
seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL)) {
v->input_read_done = 1;
} else {
ff_format_io_close(v->parent, &v->input);
+ if (c->abr) {
+ if (c->switch_delay > 0)
+ c->switch_delay--;
+ av_log(v->parent, AV_LOG_VERBOSE, "read_data: close pls[%d]->input, v->cur_seq_no=%d, c->switch_delay= %d\n",
+ v->index, v->cur_seq_no, c->switch_delay);
+ }
}
v->cur_seq_no++;
@@ -1860,6 +2015,17 @@ static int hls_read_header(AVFormatContext *s)
c->first_packet = 1;
c->first_timestamp = AV_NOPTS_VALUE;
c->cur_timestamp = AV_NOPTS_VALUE;
+ c->switch_request2 = -1;
+ c->switch_delay = 0;
+ c->switch_timestamp = AV_NOPTS_VALUE;
+ c->delta_timestamp = AV_NOPTS_VALUE;
+ c->can_switch = -1;
+
+ if (c->abr) {
+ c->http_persistent = 0;
+ c->http_multiple = 1;
+ c->throughputs = av_mallocz(sizeof(struct throughput));
+ }
if ((ret = save_avio_options(s)) < 0)
goto fail;
@@ -2047,6 +2213,9 @@ static int hls_read_header(AVFormatContext *s)
add_metadata_from_renditions(s, pls, AVMEDIA_TYPE_SUBTITLE);
}
+ if (c->abr && c->can_switch == -1)
+ c->can_switch = 1;
+
update_noheader_flag(s);
return 0;
@@ -2055,6 +2224,13 @@ fail:
return ret;
}
+static void change_discard_flags(struct playlist *pls, int flag)
+{
+ for (int i = 0; i < pls->n_main_streams; i++) {
+ pls->main_streams[i]->discard = flag;
+ }
+}
+
static int recheck_discard_flags(AVFormatContext *s, int first)
{
HLSContext *c = s->priv_data;
@@ -2065,6 +2241,22 @@ static int recheck_discard_flags(AVFormatContext *s, int first)
for (i = 0; i < c->n_playlists; i++) {
struct playlist *pls = c->playlists[i];
+ if (c->abr) {
+ if (c->switch_request2 == -1)
+ ;
+ else if (c->switch_request2 == i && c->switch_delay <= 0
+ && c->cur_timestamp + c->delta_timestamp >= c->switch_timestamp
+ && c->switch_timestamp != AV_NOPTS_VALUE) {
+ av_log(s, AV_LOG_VERBOSE, "[switch point] cur_timestamp:%ld\n", c->cur_timestamp);
+ change_discard_flags(pls, AVDISCARD_DEFAULT);
+ c->switch_timestamp = AV_NOPTS_VALUE;
+ } else if (c->switch_request2 != -1 && c->switch_request2 != i && c->switch_delay <= 0
+ && c->cur_timestamp + c->delta_timestamp >= c->switch_timestamp
+ && c->switch_timestamp != AV_NOPTS_VALUE) {
+ change_discard_flags(pls, AVDISCARD_ALL);
+ }
+ }
+
cur_needed = playlist_needed(c->playlists[i]);
if (pls->broken) {
@@ -2075,6 +2267,11 @@ static int recheck_discard_flags(AVFormatContext *s, int first)
changed = 1;
pls->cur_seq_no = select_cur_seq_no(c, pls);
pls->pb.eof_reached = 0;
+ if (c->abr) {
+ pls->cur_seq_no = select_cur_seq_no(c, pls) + 1;
+ avio_flush(&pls->pb);
+ avformat_flush(pls->ctx);
+ }
if (c->cur_timestamp != AV_NOPTS_VALUE) {
/* catch up */
pls->seek_timestamp = c->cur_timestamp;
@@ -2082,7 +2279,7 @@ static int recheck_discard_flags(AVFormatContext *s, int first)
pls->seek_stream_index = -1;
}
av_log(s, AV_LOG_INFO, "Now receiving playlist %d, segment %d\n", i, pls->cur_seq_no);
- } else if (first && !cur_needed && pls->needed) {
+ } else if ((first || c->abr) && !cur_needed && pls->needed) {
ff_format_io_close(pls->parent, &pls->input);
pls->input_read_done = 0;
ff_format_io_close(pls->parent, &pls->input_next);
@@ -2091,6 +2288,9 @@ static int recheck_discard_flags(AVFormatContext *s, int first)
changed = 1;
av_log(s, AV_LOG_INFO, "No longer receiving playlist %d\n", i);
}
+
+ if (c->abr && changed && cur_needed)
+ c->cur_pls = i;
}
return changed;
}
@@ -2165,7 +2365,13 @@ static int hls_read_packet(AVFormatContext *s, AVPacket *pkt)
/* audio elementary streams are id3 timestamped */
fill_timing_for_id3_timestamped_stream(pls);
}
-
+ if (c->abr && c->first_timestamp != AV_NOPTS_VALUE && c->delta_timestamp == AV_NOPTS_VALUE && !i) {
+ c->delta_timestamp = av_rescale_q(pls->pkt.dts,
+ get_timebase(pls), AV_TIME_BASE_Q) - c->first_timestamp + 1;
+ av_log(s, AV_LOG_VERBOSE, "[abr] delta_timestamp=%ld, %ld, %ld\n",
+ c->delta_timestamp, c->first_timestamp,av_rescale_q(pls->pkt.dts,
+ get_timebase(pls), AV_TIME_BASE_Q));
+ }
if (c->first_timestamp == AV_NOPTS_VALUE &&
pls->pkt.dts != AV_NOPTS_VALUE)
c->first_timestamp = av_rescale_q(pls->pkt.dts,
@@ -2373,6 +2579,8 @@ static int hls_probe(const AVProbeData *p)
#define OFFSET(x) offsetof(HLSContext, x)
#define FLAGS AV_OPT_FLAG_DECODING_PARAM
static const AVOption hls_options[] = {
+ {"abr", "enable abr to switch streams",
+ OFFSET(abr), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, FLAGS },
{"live_start_index", "segment index to start live streams at (negative values are from the end)",
OFFSET(live_start_index), AV_OPT_TYPE_INT, {.i64 = -3}, INT_MIN, INT_MAX, FLAGS},
{"allowed_extensions", "List of file extensions that hls is allowed to access",