@@ -249,6 +249,9 @@ DASH-templated name to used for the media segments. Default is "chunk-stream$Rep
URL of the page that will return the UTC timestamp in ISO format. Example: "https://time.akamai.com/?iso"
@item -http_user_agent @var{user_agent}
Override User-Agent field in HTTP header. Applicable only for HTTP output.
+@item -hls_playlist @var{hls_playlist}
+Generate HLS playlist files as well. The master playlist is generated with the filename master.m3u8.
+One media playlist file is generated for each stream with filenames media_0.m3u8, media_1.m3u8, etc.
@item -adaptation_sets @var{adaptation_sets}
Assign streams to AdaptationSets. Syntax is "id=x,streams=a,b,c id=y,streams=d,e" with x and y being the IDs
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
OBJS-$(CONFIG_CRC_MUXER) += crcenc.o
OBJS-$(CONFIG_DATA_DEMUXER) += rawdec.o
OBJS-$(CONFIG_DATA_MUXER) += rawenc.o
-OBJS-$(CONFIG_DASH_MUXER) += dash.o dashenc.o
+OBJS-$(CONFIG_DASH_MUXER) += dash.o dashenc.o hlsplaylist.o
OBJS-$(CONFIG_DASH_DEMUXER) += dash.o dashdec.o
OBJS-$(CONFIG_DAUD_DEMUXER) += dauddec.o
OBJS-$(CONFIG_DAUD_MUXER) += daudenc.o
@@ -36,6 +36,7 @@
#include "avc.h"
#include "avformat.h"
#include "avio_internal.h"
+#include "hlsplaylist.h"
#include "internal.h"
#include "isom.h"
#include "os_support.h"
@@ -101,6 +102,8 @@ typedef struct DASHContext {
const char *media_seg_name;
const char *utc_timing_url;
const char *user_agent;
+ int hls_playlist;
+ int master_playlist_created;
} DASHContext;
static struct codec_string {
@@ -217,6 +220,14 @@ static void set_http_options(AVDictionary **options, DASHContext *c)
av_dict_set(options, "user_agent", c->user_agent, 0);
}
+static void get_hls_playlist_name(char *playlist_name, int string_size,
+ const char *base_url, int id) {
+ if (base_url)
+ snprintf(playlist_name, string_size, "%smedia_%d.m3u8", base_url, id);
+ else
+ snprintf(playlist_name, string_size, "media_%d.m3u8", id);
+}
+
static int flush_init_segment(AVFormatContext *s, OutputStream *os)
{
DASHContext *c = s->priv_data;
@@ -262,7 +273,8 @@ static void dash_free(AVFormatContext *s)
av_freep(&c->streams);
}
-static void output_segment_list(OutputStream *os, AVIOContext *out, DASHContext *c)
+static void output_segment_list(OutputStream *os, AVIOContext *out, DASHContext *c,
+ int representation_id, int final)
{
int i, start_index = 0, start_number = 1;
if (c->window_size) {
@@ -322,6 +334,55 @@ static void output_segment_list(OutputStream *os, AVIOContext *out, DASHContext
}
avio_printf(out, "\t\t\t\t</SegmentList>\n");
}
+ if (c->hls_playlist && start_index < os->nb_segments)
+ {
+ int timescale = os->ctx->streams[0]->time_base.den;
+ char temp_filename_hls[1024];
+ char filename_hls[1024];
+ AVIOContext *out_hls = NULL;
+ AVDictionary *http_opts = NULL;
+ int target_duration = 0;
+ const char *proto = avio_find_protocol_name(c->dirname);
+ int use_rename = proto && !strcmp(proto, "file");
+
+ get_hls_playlist_name(filename_hls, sizeof(filename_hls),
+ c->dirname, representation_id);
+
+ snprintf(temp_filename_hls, sizeof(temp_filename_hls), use_rename ? "%s.tmp" : "%s", filename_hls);
+
+ set_http_options(&http_opts, c);
+ avio_open2(&out_hls, temp_filename_hls, AVIO_FLAG_WRITE, NULL, &http_opts);
+ av_dict_free(&http_opts);
+ for (i = start_index; i < os->nb_segments; i++) {
+ Segment *seg = os->segments[i];
+ double duration = (double) seg->duration / timescale;
+ if (target_duration <= duration)
+ target_duration = hls_get_int_from_double(duration);
+ }
+
+ ff_hls_write_playlist_header(out_hls, 6, -1, target_duration,
+ start_number, PLAYLIST_TYPE_NONE);
+
+ ff_hls_write_init_file(out_hls, os->initfile, c->single_file,
+ os->init_range_length, os->init_start_pos);
+
+ for (i = start_index; i < os->nb_segments; i++) {
+ Segment *seg = os->segments[i];
+ ff_hls_write_file_entry(out_hls, 0, c->single_file,
+ (double) seg->duration / timescale, 0,
+ seg->range_length, seg->start_pos, NULL,
+ c->single_file ? os->initfile : seg->file,
+ NULL);
+ }
+
+ if (final)
+ ff_hls_write_end_list(out_hls);
+
+ avio_close(out_hls);
+ if (use_rename)
+ avpriv_io_move(temp_filename_hls, filename_hls);
+ }
+
}
static char *xmlescape(const char *str) {
@@ -391,7 +452,8 @@ static void format_date_now(char *buf, int size)
}
}
-static int write_adaptation_set(AVFormatContext *s, AVIOContext *out, int as_index)
+static int write_adaptation_set(AVFormatContext *s, AVIOContext *out, int as_index,
+ int final)
{
DASHContext *c = s->priv_data;
AdaptationSet *as = &c->as[as_index];
@@ -430,7 +492,7 @@ static int write_adaptation_set(AVFormatContext *s, AVIOContext *out, int as_ind
avio_printf(out, "\t\t\t\t<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"%d\" />\n",
s->streams[i]->codecpar->channels);
}
- output_segment_list(os, out, c);
+ output_segment_list(os, out, c, i, final);
avio_printf(out, "\t\t\t</Representation>\n");
}
avio_printf(out, "\t\t</AdaptationSet>\n");
@@ -650,7 +712,7 @@ static int write_manifest(AVFormatContext *s, int final)
}
for (i = 0; i < c->nb_as; i++) {
- if ((ret = write_adaptation_set(s, out, i)) < 0)
+ if ((ret = write_adaptation_set(s, out, i, final)) < 0)
return ret;
}
avio_printf(out, "\t</Period>\n");
@@ -662,8 +724,43 @@ static int write_manifest(AVFormatContext *s, int final)
avio_flush(out);
ff_format_io_close(s, &out);
- if (use_rename)
- return avpriv_io_move(temp_filename, s->filename);
+ if (use_rename) {
+ if ((ret = avpriv_io_move(temp_filename, s->filename)) < 0)
+ return ret;
+ }
+
+ if (c->hls_playlist && !c->master_playlist_created) {
+ char filename_hls[1024];
+
+ if (*c->dirname)
+ snprintf(filename_hls, sizeof(filename_hls), "%s/master.m3u8", c->dirname);
+ else
+ snprintf(filename_hls, sizeof(filename_hls), "master.m3u8");
+
+ snprintf(temp_filename, sizeof(temp_filename), use_rename ? "%s.tmp" : "%s", filename_hls);
+
+ set_http_options(&opts, c);
+ ret = avio_open2(&out, temp_filename, AVIO_FLAG_WRITE, NULL, &opts);
+ if (ret < 0) {
+ av_log(s, AV_LOG_ERROR, "Unable to open %s for writing\n", temp_filename);
+ return ret;
+ }
+ av_dict_free(&opts);
+
+ ff_hls_write_playlist_version(out, 6);
+
+ for (i = 0; i < s->nb_streams; i++) {
+ char playlist_file[64];
+ AVStream *st = s->streams[i];
+ get_hls_playlist_name(playlist_file, sizeof(playlist_file), NULL, i);
+ ff_hls_write_stream_info(st, out, st->codecpar->bit_rate, playlist_file);
+ }
+ avio_close(out);
+ if (use_rename)
+ if ((ret = avpriv_io_move(temp_filename, filename_hls)) < 0)
+ return ret;
+ c->master_playlist_created = 1;
+ }
return 0;
}
@@ -1206,6 +1303,7 @@ static const AVOption options[] = {
{ "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 },
{ "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 },
{ "http_user_agent", "override User-Agent field in HTTP header", OFFSET(user_agent), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, E},
+ { "hls_playlist", "Generate HLS playlist files(master.m3u8, media_%d.m3u8)", OFFSET(hls_playlist), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, E },
{ NULL },
};