This is to take full advantage of Common Media Application Format(CMAF).
Now server can generate one content and serve both HLS and DASH players.
Reviewed-by: Steven Liu <lq@onvideo.cn>
| ... | ... |
@@ -249,6 +249,9 @@ DASH-templated name to used for the media segments. Default is "chunk-stream$Rep |
| 249 | 249 |
URL of the page that will return the UTC timestamp in ISO format. Example: "https://time.akamai.com/?iso" |
| 250 | 250 |
@item -http_user_agent @var{user_agent}
|
| 251 | 251 |
Override User-Agent field in HTTP header. Applicable only for HTTP output. |
| 252 |
+@item -hls_playlist @var{hls_playlist}
|
|
| 253 |
+Generate HLS playlist files as well. The master playlist is generated with the filename master.m3u8. |
|
| 254 |
+One media playlist file is generated for each stream with filenames media_0.m3u8, media_1.m3u8, etc. |
|
| 252 | 255 |
@item -adaptation_sets @var{adaptation_sets}
|
| 253 | 256 |
Assign streams to AdaptationSets. Syntax is "id=x,streams=a,b,c id=y,streams=d,e" with x and y being the IDs |
| 254 | 257 |
of the adaptation sets and a,b,c,d and e are the indices of the mapped streams. |
| ... | ... |
@@ -135,7 +135,7 @@ OBJS-$(CONFIG_CONCAT_DEMUXER) += concatdec.o |
| 135 | 135 |
OBJS-$(CONFIG_CRC_MUXER) += crcenc.o |
| 136 | 136 |
OBJS-$(CONFIG_DATA_DEMUXER) += rawdec.o |
| 137 | 137 |
OBJS-$(CONFIG_DATA_MUXER) += rawenc.o |
| 138 |
-OBJS-$(CONFIG_DASH_MUXER) += dash.o dashenc.o |
|
| 138 |
+OBJS-$(CONFIG_DASH_MUXER) += dash.o dashenc.o hlsplaylist.o |
|
| 139 | 139 |
OBJS-$(CONFIG_DASH_DEMUXER) += dash.o dashdec.o |
| 140 | 140 |
OBJS-$(CONFIG_DAUD_DEMUXER) += dauddec.o |
| 141 | 141 |
OBJS-$(CONFIG_DAUD_MUXER) += daudenc.o |
| ... | ... |
@@ -36,6 +36,7 @@ |
| 36 | 36 |
#include "avc.h" |
| 37 | 37 |
#include "avformat.h" |
| 38 | 38 |
#include "avio_internal.h" |
| 39 |
+#include "hlsplaylist.h" |
|
| 39 | 40 |
#include "internal.h" |
| 40 | 41 |
#include "isom.h" |
| 41 | 42 |
#include "os_support.h" |
| ... | ... |
@@ -101,6 +102,8 @@ typedef struct DASHContext {
|
| 101 | 101 |
const char *media_seg_name; |
| 102 | 102 |
const char *utc_timing_url; |
| 103 | 103 |
const char *user_agent; |
| 104 |
+ int hls_playlist; |
|
| 105 |
+ int master_playlist_created; |
|
| 104 | 106 |
} DASHContext; |
| 105 | 107 |
|
| 106 | 108 |
static struct codec_string {
|
| ... | ... |
@@ -217,6 +220,14 @@ static void set_http_options(AVDictionary **options, DASHContext *c) |
| 217 | 217 |
av_dict_set(options, "user_agent", c->user_agent, 0); |
| 218 | 218 |
} |
| 219 | 219 |
|
| 220 |
+static void get_hls_playlist_name(char *playlist_name, int string_size, |
|
| 221 |
+ const char *base_url, int id) {
|
|
| 222 |
+ if (base_url) |
|
| 223 |
+ snprintf(playlist_name, string_size, "%smedia_%d.m3u8", base_url, id); |
|
| 224 |
+ else |
|
| 225 |
+ snprintf(playlist_name, string_size, "media_%d.m3u8", id); |
|
| 226 |
+} |
|
| 227 |
+ |
|
| 220 | 228 |
static int flush_init_segment(AVFormatContext *s, OutputStream *os) |
| 221 | 229 |
{
|
| 222 | 230 |
DASHContext *c = s->priv_data; |
| ... | ... |
@@ -262,7 +273,8 @@ static void dash_free(AVFormatContext *s) |
| 262 | 262 |
av_freep(&c->streams); |
| 263 | 263 |
} |
| 264 | 264 |
|
| 265 |
-static void output_segment_list(OutputStream *os, AVIOContext *out, DASHContext *c) |
|
| 265 |
+static void output_segment_list(OutputStream *os, AVIOContext *out, DASHContext *c, |
|
| 266 |
+ int representation_id, int final) |
|
| 266 | 267 |
{
|
| 267 | 268 |
int i, start_index = 0, start_number = 1; |
| 268 | 269 |
if (c->window_size) {
|
| ... | ... |
@@ -322,6 +334,55 @@ static void output_segment_list(OutputStream *os, AVIOContext *out, DASHContext |
| 322 | 322 |
} |
| 323 | 323 |
avio_printf(out, "\t\t\t\t</SegmentList>\n"); |
| 324 | 324 |
} |
| 325 |
+ if (c->hls_playlist && start_index < os->nb_segments) |
|
| 326 |
+ {
|
|
| 327 |
+ int timescale = os->ctx->streams[0]->time_base.den; |
|
| 328 |
+ char temp_filename_hls[1024]; |
|
| 329 |
+ char filename_hls[1024]; |
|
| 330 |
+ AVIOContext *out_hls = NULL; |
|
| 331 |
+ AVDictionary *http_opts = NULL; |
|
| 332 |
+ int target_duration = 0; |
|
| 333 |
+ const char *proto = avio_find_protocol_name(c->dirname); |
|
| 334 |
+ int use_rename = proto && !strcmp(proto, "file"); |
|
| 335 |
+ |
|
| 336 |
+ get_hls_playlist_name(filename_hls, sizeof(filename_hls), |
|
| 337 |
+ c->dirname, representation_id); |
|
| 338 |
+ |
|
| 339 |
+ snprintf(temp_filename_hls, sizeof(temp_filename_hls), use_rename ? "%s.tmp" : "%s", filename_hls); |
|
| 340 |
+ |
|
| 341 |
+ set_http_options(&http_opts, c); |
|
| 342 |
+ avio_open2(&out_hls, temp_filename_hls, AVIO_FLAG_WRITE, NULL, &http_opts); |
|
| 343 |
+ av_dict_free(&http_opts); |
|
| 344 |
+ for (i = start_index; i < os->nb_segments; i++) {
|
|
| 345 |
+ Segment *seg = os->segments[i]; |
|
| 346 |
+ double duration = (double) seg->duration / timescale; |
|
| 347 |
+ if (target_duration <= duration) |
|
| 348 |
+ target_duration = hls_get_int_from_double(duration); |
|
| 349 |
+ } |
|
| 350 |
+ |
|
| 351 |
+ ff_hls_write_playlist_header(out_hls, 6, -1, target_duration, |
|
| 352 |
+ start_number, PLAYLIST_TYPE_NONE); |
|
| 353 |
+ |
|
| 354 |
+ ff_hls_write_init_file(out_hls, os->initfile, c->single_file, |
|
| 355 |
+ os->init_range_length, os->init_start_pos); |
|
| 356 |
+ |
|
| 357 |
+ for (i = start_index; i < os->nb_segments; i++) {
|
|
| 358 |
+ Segment *seg = os->segments[i]; |
|
| 359 |
+ ff_hls_write_file_entry(out_hls, 0, c->single_file, |
|
| 360 |
+ (double) seg->duration / timescale, 0, |
|
| 361 |
+ seg->range_length, seg->start_pos, NULL, |
|
| 362 |
+ c->single_file ? os->initfile : seg->file, |
|
| 363 |
+ NULL); |
|
| 364 |
+ } |
|
| 365 |
+ |
|
| 366 |
+ if (final) |
|
| 367 |
+ ff_hls_write_end_list(out_hls); |
|
| 368 |
+ |
|
| 369 |
+ avio_close(out_hls); |
|
| 370 |
+ if (use_rename) |
|
| 371 |
+ avpriv_io_move(temp_filename_hls, filename_hls); |
|
| 372 |
+ } |
|
| 373 |
+ |
|
| 325 | 374 |
} |
| 326 | 375 |
|
| 327 | 376 |
static char *xmlescape(const char *str) {
|
| ... | ... |
@@ -391,7 +452,8 @@ static void format_date_now(char *buf, int size) |
| 391 | 391 |
} |
| 392 | 392 |
} |
| 393 | 393 |
|
| 394 |
-static int write_adaptation_set(AVFormatContext *s, AVIOContext *out, int as_index) |
|
| 394 |
+static int write_adaptation_set(AVFormatContext *s, AVIOContext *out, int as_index, |
|
| 395 |
+ int final) |
|
| 395 | 396 |
{
|
| 396 | 397 |
DASHContext *c = s->priv_data; |
| 397 | 398 |
AdaptationSet *as = &c->as[as_index]; |
| ... | ... |
@@ -430,7 +492,7 @@ static int write_adaptation_set(AVFormatContext *s, AVIOContext *out, int as_ind |
| 430 | 430 |
avio_printf(out, "\t\t\t\t<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"%d\" />\n", |
| 431 | 431 |
s->streams[i]->codecpar->channels); |
| 432 | 432 |
} |
| 433 |
- output_segment_list(os, out, c); |
|
| 433 |
+ output_segment_list(os, out, c, i, final); |
|
| 434 | 434 |
avio_printf(out, "\t\t\t</Representation>\n"); |
| 435 | 435 |
} |
| 436 | 436 |
avio_printf(out, "\t\t</AdaptationSet>\n"); |
| ... | ... |
@@ -650,7 +712,7 @@ static int write_manifest(AVFormatContext *s, int final) |
| 650 | 650 |
} |
| 651 | 651 |
|
| 652 | 652 |
for (i = 0; i < c->nb_as; i++) {
|
| 653 |
- if ((ret = write_adaptation_set(s, out, i)) < 0) |
|
| 653 |
+ if ((ret = write_adaptation_set(s, out, i, final)) < 0) |
|
| 654 | 654 |
return ret; |
| 655 | 655 |
} |
| 656 | 656 |
avio_printf(out, "\t</Period>\n"); |
| ... | ... |
@@ -662,8 +724,43 @@ static int write_manifest(AVFormatContext *s, int final) |
| 662 | 662 |
avio_flush(out); |
| 663 | 663 |
ff_format_io_close(s, &out); |
| 664 | 664 |
|
| 665 |
- if (use_rename) |
|
| 666 |
- return avpriv_io_move(temp_filename, s->filename); |
|
| 665 |
+ if (use_rename) {
|
|
| 666 |
+ if ((ret = avpriv_io_move(temp_filename, s->filename)) < 0) |
|
| 667 |
+ return ret; |
|
| 668 |
+ } |
|
| 669 |
+ |
|
| 670 |
+ if (c->hls_playlist && !c->master_playlist_created) {
|
|
| 671 |
+ char filename_hls[1024]; |
|
| 672 |
+ |
|
| 673 |
+ if (*c->dirname) |
|
| 674 |
+ snprintf(filename_hls, sizeof(filename_hls), "%s/master.m3u8", c->dirname); |
|
| 675 |
+ else |
|
| 676 |
+ snprintf(filename_hls, sizeof(filename_hls), "master.m3u8"); |
|
| 677 |
+ |
|
| 678 |
+ snprintf(temp_filename, sizeof(temp_filename), use_rename ? "%s.tmp" : "%s", filename_hls); |
|
| 679 |
+ |
|
| 680 |
+ set_http_options(&opts, c); |
|
| 681 |
+ ret = avio_open2(&out, temp_filename, AVIO_FLAG_WRITE, NULL, &opts); |
|
| 682 |
+ if (ret < 0) {
|
|
| 683 |
+ av_log(s, AV_LOG_ERROR, "Unable to open %s for writing\n", temp_filename); |
|
| 684 |
+ return ret; |
|
| 685 |
+ } |
|
| 686 |
+ av_dict_free(&opts); |
|
| 687 |
+ |
|
| 688 |
+ ff_hls_write_playlist_version(out, 6); |
|
| 689 |
+ |
|
| 690 |
+ for (i = 0; i < s->nb_streams; i++) {
|
|
| 691 |
+ char playlist_file[64]; |
|
| 692 |
+ AVStream *st = s->streams[i]; |
|
| 693 |
+ get_hls_playlist_name(playlist_file, sizeof(playlist_file), NULL, i); |
|
| 694 |
+ ff_hls_write_stream_info(st, out, st->codecpar->bit_rate, playlist_file); |
|
| 695 |
+ } |
|
| 696 |
+ avio_close(out); |
|
| 697 |
+ if (use_rename) |
|
| 698 |
+ if ((ret = avpriv_io_move(temp_filename, filename_hls)) < 0) |
|
| 699 |
+ return ret; |
|
| 700 |
+ c->master_playlist_created = 1; |
|
| 701 |
+ } |
|
| 667 | 702 |
|
| 668 | 703 |
return 0; |
| 669 | 704 |
} |
| ... | ... |
@@ -1206,6 +1303,7 @@ static const AVOption options[] = {
|
| 1206 | 1206 |
{ "media_seg_name", "DASH-templated name to used for the media segments", OFFSET(media_seg_name), AV_OPT_TYPE_STRING, {.str = "chunk-stream$RepresentationID$-$Number%05d$.m4s"}, 0, 0, E },
|
| 1207 | 1207 |
{ "utc_timing_url", "URL of the page that will return the UTC timestamp in ISO format", OFFSET(utc_timing_url), AV_OPT_TYPE_STRING, { 0 }, 0, 0, E },
|
| 1208 | 1208 |
{ "http_user_agent", "override User-Agent field in HTTP header", OFFSET(user_agent), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E},
|
| 1209 |
+ { "hls_playlist", "Generate HLS playlist files(master.m3u8, media_%d.m3u8)", OFFSET(hls_playlist), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, E },
|
|
| 1209 | 1210 |
{ NULL },
|
| 1210 | 1211 |
}; |
| 1211 | 1212 |
|