hlsenc: single_file, support HLS ver 4 byteranges
authorMika Raento <mika.raento@elisa.fi>
Mon, 15 Sep 2014 13:26:57 +0000 (16:26 +0300)
committerMichael Niedermayer <michaelni@gmx.at>
Mon, 15 Sep 2014 20:16:44 +0000 (22:16 +0200)
This adds a new option -hls_flags single_file that creates one .ts file
for HLS and adds byteranges to the .m3u8 file, instead of creating one
.ts file for each segment.

This is helpful at least for storing large number of videos, as the
number of files per video is drastically reduced and copying and storing
those files takes less requests and inodes.

This is based on work by Nicolas Martyanoff, discussed on ffmpeg-devel
in July 2014. That patch seems abandoned by the author, and contained
unrelated changes. This patch tries to add the minimum amount of code to
support the byterange playlists.

Signed-off-by: Michael Niedermayer <michaelni@gmx.at>
doc/muxers.texi
libavformat/hlsenc.c

index 57e81f49fbaf1e82fa914ce34a40bca3dd50b3b3..40ae857040df4270af92758c0f1011e2024857d8 100644 (file)
@@ -194,15 +194,19 @@ can not be smaller than one centi second.
 Apple HTTP Live Streaming muxer that segments MPEG-TS according to
 the HTTP Live Streaming (HLS) specification.
 
 Apple HTTP Live Streaming muxer that segments MPEG-TS according to
 the HTTP Live Streaming (HLS) specification.
 
-It creates a playlist file and numbered segment files. The output
-filename specifies the playlist filename; the segment filenames
-receive the same basename as the playlist, a sequential number and
-a .ts extension.
+It creates a playlist file, and one or more segment files. The output filename
+specifies the playlist filename.
+
+By default, the muxer creates a file for each segment produced. These files
+have the same name as the playlist, followed by a sequential number and a
+.ts extension.
 
 For example, to convert an input file with @command{ffmpeg}:
 @example
 ffmpeg -i in.nut out.m3u8
 @end example
 
 For example, to convert an input file with @command{ffmpeg}:
 @example
 ffmpeg -i in.nut out.m3u8
 @end example
+This example will produce the playlist, @file{out.m3u8}, and segment files:
+@file{out0.ts}, @file{out1.ts}, @file{out2.ts}, etc.
 
 See also the @ref{segment} muxer, which provides a more generic and
 flexible implementation of a segmenter, and can be used to perform HLS
 
 See also the @ref{segment} muxer, which provides a more generic and
 flexible implementation of a segmenter, and can be used to perform HLS
@@ -241,6 +245,17 @@ Note that the playlist sequence number must be unique for each segment
 and it is not to be confused with the segment filename sequence number
 which can be cyclic, for example if the @option{wrap} option is
 specified.
 and it is not to be confused with the segment filename sequence number
 which can be cyclic, for example if the @option{wrap} option is
 specified.
+
+@item hls_flags single_file
+If this flag is set, the muxer will store all segments in a single MPEG-TS
+file, and will use byte ranges in the playlist. HLS playlists generated with
+this way will have the version number 4.
+For example:
+@example
+ffmpeg -i in.nut -hls_flags single_file out.m3u8
+@end example
+Will produce the playlist, @file{out.m3u8}, and a single segment file,
+@file{out.ts}.
 @end table
 
 @anchor{ico}
 @end table
 
 @anchor{ico}
index 11f1e5be42faebcfc975e309a381328f365c0349..c03737a3848769b019127f95d444e24848aea3fb 100644 (file)
 typedef struct HLSSegment {
     char filename[1024];
     double duration; /* in seconds */
 typedef struct HLSSegment {
     char filename[1024];
     double duration; /* in seconds */
+    int64_t pos;
+    int64_t size;
 
     struct HLSSegment *next;
 } HLSSegment;
 
 
     struct HLSSegment *next;
 } HLSSegment;
 
+typedef enum HLSFlags {
+    // Generate a single media file and use byte ranges in the playlist.
+    HLS_SINGLE_FILE = (1 << 0),
+} HLSFlags;
+
 typedef struct HLSContext {
     const AVClass *class;  // Class for private options.
     unsigned number;
 typedef struct HLSContext {
     const AVClass *class;  // Class for private options.
     unsigned number;
@@ -50,12 +57,15 @@ typedef struct HLSContext {
     float time;            // Set by a private option.
     int max_nb_segments;   // Set by a private option.
     int  wrap;             // Set by a private option.
     float time;            // Set by a private option.
     int max_nb_segments;   // Set by a private option.
     int  wrap;             // Set by a private option.
+    uint32_t flags;        // enum HLSFlags
 
     int64_t recording_time;
     int has_video;
     int64_t start_pts;
     int64_t end_pts;
     double duration;      // last segment duration computed so far, in seconds
 
     int64_t recording_time;
     int has_video;
     int64_t start_pts;
     int64_t end_pts;
     double duration;      // last segment duration computed so far, in seconds
+    int64_t start_pos;    // last segment starting position
+    int64_t size;         // last segment size
     int nb_entries;
 
     HLSSegment *segments;
     int nb_entries;
 
     HLSSegment *segments;
@@ -88,12 +98,14 @@ static int hls_mux_init(AVFormatContext *s)
         avcodec_copy_context(st->codec, s->streams[i]->codec);
         st->sample_aspect_ratio = s->streams[i]->sample_aspect_ratio;
     }
         avcodec_copy_context(st->codec, s->streams[i]->codec);
         st->sample_aspect_ratio = s->streams[i]->sample_aspect_ratio;
     }
+    hls->start_pos = 0;
 
     return 0;
 }
 
 /* Create a new segment and append it to the segment list */
 
     return 0;
 }
 
 /* Create a new segment and append it to the segment list */
-static int hls_append_segment(HLSContext *hls, double duration)
+static int hls_append_segment(HLSContext *hls, double duration, int64_t pos,
+                              int64_t size)
 {
     HLSSegment *en = av_malloc(sizeof(*en));
 
 {
     HLSSegment *en = av_malloc(sizeof(*en));
 
@@ -103,6 +115,8 @@ static int hls_append_segment(HLSContext *hls, double duration)
     av_strlcpy(en->filename, av_basename(hls->avf->filename), sizeof(en->filename));
 
     en->duration = duration;
     av_strlcpy(en->filename, av_basename(hls->avf->filename), sizeof(en->filename));
 
     en->duration = duration;
+    en->pos      = pos;
+    en->size     = size;
     en->next     = NULL;
 
     if (!hls->segments)
     en->next     = NULL;
 
     if (!hls->segments)
@@ -142,6 +156,7 @@ static int hls_window(AVFormatContext *s, int last)
     int target_duration = 0;
     int ret = 0;
     int64_t sequence = FFMAX(hls->start_sequence, hls->sequence - hls->nb_entries);
     int target_duration = 0;
     int ret = 0;
     int64_t sequence = FFMAX(hls->start_sequence, hls->sequence - hls->nb_entries);
+    int version = hls->flags & HLS_SINGLE_FILE ? 4 : 3;
 
     if ((ret = avio_open2(&hls->pb, s->filename, AVIO_FLAG_WRITE,
                           &s->interrupt_callback, NULL)) < 0)
 
     if ((ret = avio_open2(&hls->pb, s->filename, AVIO_FLAG_WRITE,
                           &s->interrupt_callback, NULL)) < 0)
@@ -153,7 +168,7 @@ static int hls_window(AVFormatContext *s, int last)
     }
 
     avio_printf(hls->pb, "#EXTM3U\n");
     }
 
     avio_printf(hls->pb, "#EXTM3U\n");
-    avio_printf(hls->pb, "#EXT-X-VERSION:3\n");
+    avio_printf(hls->pb, "#EXT-X-VERSION:%d\n", version);
     avio_printf(hls->pb, "#EXT-X-TARGETDURATION:%d\n", target_duration);
     avio_printf(hls->pb, "#EXT-X-MEDIA-SEQUENCE:%"PRId64"\n", sequence);
 
     avio_printf(hls->pb, "#EXT-X-TARGETDURATION:%d\n", target_duration);
     avio_printf(hls->pb, "#EXT-X-MEDIA-SEQUENCE:%"PRId64"\n", sequence);
 
@@ -162,6 +177,9 @@ static int hls_window(AVFormatContext *s, int last)
 
     for (en = hls->segments; en; en = en->next) {
         avio_printf(hls->pb, "#EXTINF:%f,\n", en->duration);
 
     for (en = hls->segments; en; en = en->next) {
         avio_printf(hls->pb, "#EXTINF:%f,\n", en->duration);
+        if (hls->flags & HLS_SINGLE_FILE)
+             avio_printf(hls->pb, "#EXT-X-BYTERANGE:%"PRIi64"@%"PRIi64"\n",
+                         en->size, en->pos);
         if (hls->baseurl)
             avio_printf(hls->pb, "%s", hls->baseurl);
         avio_printf(hls->pb, "%s\n", en->filename);
         if (hls->baseurl)
             avio_printf(hls->pb, "%s", hls->baseurl);
         avio_printf(hls->pb, "%s\n", en->filename);
@@ -181,11 +199,15 @@ static int hls_start(AVFormatContext *s)
     AVFormatContext *oc = c->avf;
     int err = 0;
 
     AVFormatContext *oc = c->avf;
     int err = 0;
 
-    if (av_get_frame_filename(oc->filename, sizeof(oc->filename),
-                              c->basename, c->wrap ? c->sequence % c->wrap : c->sequence) < 0) {
-        av_log(oc, AV_LOG_ERROR, "Invalid segment filename template '%s'\n", c->basename);
-        return AVERROR(EINVAL);
-    }
+    if (c->flags & HLS_SINGLE_FILE)
+        av_strlcpy(oc->filename, c->basename,
+                   sizeof(oc->filename));
+    else
+        if (av_get_frame_filename(oc->filename, sizeof(oc->filename),
+                                  c->basename, c->wrap ? c->sequence % c->wrap : c->sequence) < 0) {
+            av_log(oc, AV_LOG_ERROR, "Invalid segment filename template '%s'\n", c->basename);
+            return AVERROR(EINVAL);
+        }
     c->number++;
 
     if ((err = avio_open2(&oc->pb, oc->filename, AVIO_FLAG_WRITE,
     c->number++;
 
     if ((err = avio_open2(&oc->pb, oc->filename, AVIO_FLAG_WRITE,
@@ -210,6 +232,9 @@ static int hls_write_header(AVFormatContext *s)
     hls->recording_time = hls->time * AV_TIME_BASE;
     hls->start_pts      = AV_NOPTS_VALUE;
 
     hls->recording_time = hls->time * AV_TIME_BASE;
     hls->start_pts      = AV_NOPTS_VALUE;
 
+    if (hls->flags & HLS_SINGLE_FILE)
+        pattern = ".ts";
+
     for (i = 0; i < s->nb_streams; i++)
         hls->has_video +=
             s->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO;
     for (i = 0; i < s->nb_streams; i++)
         hls->has_video +=
             s->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO;
@@ -289,17 +314,28 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt)
 
     if (can_split && av_compare_ts(pkt->pts - hls->start_pts, st->time_base,
                                    end_pts, AV_TIME_BASE_Q) >= 0) {
 
     if (can_split && av_compare_ts(pkt->pts - hls->start_pts, st->time_base,
                                    end_pts, AV_TIME_BASE_Q) >= 0) {
-        ret = hls_append_segment(hls, hls->duration);
+        int64_t new_start_pos;
+        av_write_frame(oc, NULL); /* Flush any buffered data */
+
+        new_start_pos = avio_tell(hls->avf->pb);
+        hls->size = new_start_pos - hls->start_pos;
+        ret = hls_append_segment(hls, hls->duration, hls->start_pos, hls->size);
+        hls->start_pos = new_start_pos;
         if (ret)
             return ret;
 
         hls->end_pts = pkt->pts;
         hls->duration = 0;
 
         if (ret)
             return ret;
 
         hls->end_pts = pkt->pts;
         hls->duration = 0;
 
-        av_write_frame(oc, NULL); /* Flush any buffered data */
-        avio_close(oc->pb);
+        if (hls->flags & HLS_SINGLE_FILE) {
+            if (hls->avf->oformat->priv_class && hls->avf->priv_data)
+                av_opt_set(hls->avf->priv_data, "mpegts_flags", "resend_headers", 0);
+            hls->number++;
+        } else {
+            avio_close(oc->pb);
 
 
-        ret = hls_start(s);
+            ret = hls_start(s);
+        }
 
         if (ret)
             return ret;
 
         if (ret)
             return ret;
@@ -321,10 +357,11 @@ static int hls_write_trailer(struct AVFormatContext *s)
     AVFormatContext *oc = hls->avf;
 
     av_write_trailer(oc);
     AVFormatContext *oc = hls->avf;
 
     av_write_trailer(oc);
+    hls->size = avio_tell(hls->avf->pb) - hls->start_pos;
     avio_closep(&oc->pb);
     avformat_free_context(oc);
     av_free(hls->basename);
     avio_closep(&oc->pb);
     avformat_free_context(oc);
     av_free(hls->basename);
-    hls_append_segment(hls, hls->duration);
+    hls_append_segment(hls, hls->duration, hls->start_pos, hls->size);
     hls_window(s, 1);
 
     hls_free_segments(hls);
     hls_window(s, 1);
 
     hls_free_segments(hls);
@@ -340,6 +377,9 @@ static const AVOption options[] = {
     {"hls_list_size", "set maximum number of playlist entries",  OFFSET(max_nb_segments),    AV_OPT_TYPE_INT,    {.i64 = 5},     0, INT_MAX, E},
     {"hls_wrap",      "set number after which the index wraps",  OFFSET(wrap),    AV_OPT_TYPE_INT,    {.i64 = 0},     0, INT_MAX, E},
     {"hls_base_url",  "url to prepend to each playlist entry",   OFFSET(baseurl), AV_OPT_TYPE_STRING, {.str = NULL},  0, 0,       E},
     {"hls_list_size", "set maximum number of playlist entries",  OFFSET(max_nb_segments),    AV_OPT_TYPE_INT,    {.i64 = 5},     0, INT_MAX, E},
     {"hls_wrap",      "set number after which the index wraps",  OFFSET(wrap),    AV_OPT_TYPE_INT,    {.i64 = 0},     0, INT_MAX, E},
     {"hls_base_url",  "url to prepend to each playlist entry",   OFFSET(baseurl), AV_OPT_TYPE_STRING, {.str = NULL},  0, 0,       E},
+    {"hls_flags",     "set flags affecting HLS playlist and media file generation", OFFSET(flags), AV_OPT_TYPE_FLAGS, {.i64 = 0 }, 0, UINT_MAX, E, "flags"},
+    {"single_file",   "generate a single media file indexed with byte ranges", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_SINGLE_FILE }, 0, UINT_MAX,   E, "flags"},
+
     { NULL },
 };
 
     { NULL },
 };