Commit 435214a7 authored by Martin Storsjö's avatar Martin Storsjö
Browse files

Add an HDS live fragmenting muxer



HDS fragments basically are FLV fragments wrapped in an ISO
media mdat atom.
Signed-off-by: default avatarMartin Storsjö <martin@martin.st>
parent dc6ea00c
......@@ -44,6 +44,7 @@ version 10:
- HEVC decoder
- raw HEVC, HEVC in MOV/MP4, HEVC in Matroska, HEVC in MPEG-TS demuxing
- remove avplay -vismv option, which has not worked for a long time
- Live HDS muxer
version 9:
......
......@@ -1825,6 +1825,7 @@ dxa_demuxer_select="riffdec"
eac3_demuxer_select="ac3_parser"
f4v_muxer_select="mov_muxer"
flac_demuxer_select="flac_parser"
hds_muxer_select="flv_muxer"
hls_muxer_select="mpegts_muxer"
ipod_muxer_select="mov_muxer"
ismv_muxer_select="mov_muxer"
......
......@@ -140,6 +140,7 @@ OBJS-$(CONFIG_H263_DEMUXER) += h263dec.o rawdec.o
OBJS-$(CONFIG_H263_MUXER) += rawenc.o
OBJS-$(CONFIG_H264_DEMUXER) += h264dec.o rawdec.o
OBJS-$(CONFIG_H264_MUXER) += rawenc.o
OBJS-$(CONFIG_HDS_MUXER) += hdsenc.o
OBJS-$(CONFIG_HEVC_DEMUXER) += hevcdec.o rawdec.o
OBJS-$(CONFIG_HLS_DEMUXER) += hls.o
OBJS-$(CONFIG_HLS_MUXER) += hlsenc.o
......
......@@ -117,6 +117,7 @@ void av_register_all(void)
REGISTER_MUXDEMUX(H261, h261);
REGISTER_MUXDEMUX(H263, h263);
REGISTER_MUXDEMUX(H264, h264);
REGISTER_MUXER (HDS, hds);
REGISTER_DEMUXER (HEVC, hevc);
REGISTER_MUXDEMUX(HLS, hls);
REGISTER_DEMUXER (HNM, hnm);
......
/*
* Live HDS fragmenter
* Copyright (c) 2013 Martin Storsjo
*
* This file is part of Libav.
*
* Libav is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* Libav is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Libav; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "config.h"
#include <float.h>
#if HAVE_UNISTD_H
#include <unistd.h>
#endif
#include "avformat.h"
#include "internal.h"
#include "os_support.h"
#include "libavutil/avstring.h"
#include "libavutil/base64.h"
#include "libavutil/intreadwrite.h"
#include "libavutil/mathematics.h"
#include "libavutil/opt.h"
typedef struct Fragment {
char file[1024];
int64_t start_time, duration;
int n;
} Fragment;
typedef struct OutputStream {
int bitrate;
int first_stream;
AVFormatContext *ctx;
int ctx_inited;
uint8_t iobuf[32768];
char temp_filename[1024];
int64_t frag_start_ts, last_ts;
AVIOContext *out;
int packets_written;
int nb_fragments, fragments_size, fragment_index;
Fragment **fragments;
int has_audio, has_video;
uint8_t *metadata;
int metadata_size;
uint8_t *extra_packets[2];
int extra_packet_sizes[2];
int nb_extra_packets;
} OutputStream;
typedef struct HDSContext {
const AVClass *class; /* Class for private options. */
int window_size;
int extra_window_size;
int min_frag_duration;
int remove_at_exit;
OutputStream *streams;
int nb_streams;
} HDSContext;
static int parse_header(OutputStream *os, const uint8_t *buf, int buf_size)
{
if (buf_size < 13)
return AVERROR_INVALIDDATA;
if (memcmp(buf, "FLV", 3))
return AVERROR_INVALIDDATA;
buf += 13;
buf_size -= 13;
while (buf_size >= 11 + 4) {
int type = buf[0];
int size = AV_RB24(&buf[1]) + 11 + 4;
if (size > buf_size)
return AVERROR_INVALIDDATA;
if (type == 8 || type == 9) {
if (os->nb_extra_packets > FF_ARRAY_ELEMS(os->extra_packets))
return AVERROR_INVALIDDATA;
os->extra_packet_sizes[os->nb_extra_packets] = size;
os->extra_packets[os->nb_extra_packets] = av_malloc(size);
if (!os->extra_packets[os->nb_extra_packets])
return AVERROR(ENOMEM);
memcpy(os->extra_packets[os->nb_extra_packets], buf, size);
os->nb_extra_packets++;
} else if (type == 0x12) {
if (os->metadata)
return AVERROR_INVALIDDATA;
os->metadata_size = size - 11 - 4;
os->metadata = av_malloc(os->metadata_size);
if (!os->metadata)
return AVERROR(ENOMEM);
memcpy(os->metadata, buf + 11, os->metadata_size);
}
buf += size;
buf_size -= size;
}
if (!os->metadata)
return AVERROR_INVALIDDATA;
return 0;
}
static int hds_write(void *opaque, uint8_t *buf, int buf_size)
{
OutputStream *os = opaque;
if (os->out) {
avio_write(os->out, buf, buf_size);
} else {
if (!os->metadata_size) {
int ret;
// Assuming the IO buffer is large enough to fit the
// FLV header and all metadata and extradata packets
if ((ret = parse_header(os, buf, buf_size)) < 0)
return ret;
}
}
return buf_size;
}
static void hds_free(AVFormatContext *s)
{
HDSContext *c = s->priv_data;
int i, j;
if (!c->streams)
return;
for (i = 0; i < s->nb_streams; i++) {
OutputStream *os = &c->streams[i];
if (os->out)
avio_close(os->out);
os->out = NULL;
if (os->ctx && os->ctx_inited)
av_write_trailer(os->ctx);
if (os->ctx && os->ctx->pb)
av_free(os->ctx->pb);
if (os->ctx)
avformat_free_context(os->ctx);
av_free(os->metadata);
for (j = 0; j < os->nb_extra_packets; j++)
av_free(os->extra_packets[j]);
for (j = 0; j < os->nb_fragments; j++)
av_free(os->fragments[j]);
av_free(os->fragments);
}
av_freep(&c->streams);
}
static int write_manifest(AVFormatContext *s, int final)
{
HDSContext *c = s->priv_data;
AVIOContext *out;
char filename[1024], temp_filename[1024];
int ret, i;
float duration = 0;
if (c->nb_streams > 0)
duration = c->streams[0].last_ts * av_q2d(s->streams[0]->time_base);
snprintf(filename, sizeof(filename), "%s/index.f4m", s->filename);
snprintf(temp_filename, sizeof(temp_filename), "%s/index.f4m.tmp", s->filename);
ret = avio_open2(&out, temp_filename, AVIO_FLAG_WRITE,
&s->interrupt_callback, NULL);
if (ret < 0) {
av_log(s, AV_LOG_ERROR, "Unable to open %s for writing\n", filename);
return ret;
}
avio_printf(out, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
avio_printf(out, "<manifest xmlns=\"http://ns.adobe.com/f4m/1.0\">\n");
avio_printf(out, "\t<id>%s</id>\n", av_basename(s->filename));
avio_printf(out, "\t<streamType>%s</streamType>\n",
final ? "recorded" : "live");
avio_printf(out, "\t<deliveryType>streaming</deliveryType>\n");
if (final)
avio_printf(out, "\t<duration>%f</duration>\n", duration);
for (i = 0; i < c->nb_streams; i++) {
OutputStream *os = &c->streams[i];
int b64_size = AV_BASE64_SIZE(os->metadata_size);
char *base64 = av_malloc(b64_size);
if (!base64) {
avio_close(out);
return AVERROR(ENOMEM);
}
av_base64_encode(base64, b64_size, os->metadata, os->metadata_size);
avio_printf(out, "\t<bootstrapInfo profile=\"named\" url=\"stream%d.abst\" id=\"bootstrap%d\" />\n", i, i);
avio_printf(out, "\t<media bitrate=\"%d\" url=\"stream%d\" bootstrapInfoId=\"bootstrap%d\">\n", os->bitrate/1000, i, i);
avio_printf(out, "\t\t<metadata>%s</metadata>\n", base64);
avio_printf(out, "\t</media>\n");
av_free(base64);
}
avio_printf(out, "</manifest>\n");
avio_flush(out);
avio_close(out);
rename(temp_filename, filename);
return 0;
}
static void update_size(AVIOContext *out, int64_t pos)
{
int64_t end = avio_tell(out);
avio_seek(out, pos, SEEK_SET);
avio_wb32(out, end - pos);
avio_seek(out, end, SEEK_SET);
}
/* Note, the .abst files need to be served with the "binary/octet"
* mime type, otherwise at least the OSMF player can easily fail
* with "stream not found" when polling for the next fragment. */
static int write_abst(AVFormatContext *s, OutputStream *os, int final)
{
HDSContext *c = s->priv_data;
AVIOContext *out;
char filename[1024], temp_filename[1024];
int i, ret;
int64_t asrt_pos, afrt_pos;
int start = 0, fragments;
int index = s->streams[os->first_stream]->id;
int64_t cur_media_time = 0;
if (c->window_size)
start = FFMAX(os->nb_fragments - c->window_size, 0);
fragments = os->nb_fragments - start;
if (final)
cur_media_time = os->last_ts;
else if (os->nb_fragments)
cur_media_time = os->fragments[os->nb_fragments - 1]->start_time;
snprintf(filename, sizeof(filename),
"%s/stream%d.abst", s->filename, index);
snprintf(temp_filename, sizeof(temp_filename),
"%s/stream%d.abst.tmp", s->filename, index);
ret = avio_open2(&out, temp_filename, AVIO_FLAG_WRITE,
&s->interrupt_callback, NULL);
if (ret < 0) {
av_log(s, AV_LOG_ERROR, "Unable to open %s for writing\n", temp_filename);
return ret;
}
avio_wb32(out, 0); // abst size
avio_wl32(out, MKTAG('a','b','s','t'));
avio_wb32(out, 0); // version + flags
avio_wb32(out, os->fragment_index - 1); // BootstrapinfoVersion
avio_w8(out, final ? 0 : 0x20); // profile, live, update
avio_wb32(out, 1000); // timescale
avio_wb64(out, cur_media_time);
avio_wb64(out, 0); // SmpteTimeCodeOffset
avio_w8(out, 0); // MovieIdentifer (null string)
avio_w8(out, 0); // ServerEntryCount
avio_w8(out, 0); // QualityEntryCount
avio_w8(out, 0); // DrmData (null string)
avio_w8(out, 0); // MetaData (null string)
avio_w8(out, 1); // SegmentRunTableCount
asrt_pos = avio_tell(out);
avio_wb32(out, 0); // asrt size
avio_wl32(out, MKTAG('a','s','r','t'));
avio_wb32(out, 0); // version + flags
avio_w8(out, 0); // QualityEntryCount
avio_wb32(out, 1); // SegmentRunEntryCount
avio_wb32(out, 1); // FirstSegment
avio_wb32(out, final ? (os->fragment_index - 1) : 0xffffffff); // FragmentsPerSegment
update_size(out, asrt_pos);
avio_w8(out, 1); // FragmentRunTableCount
afrt_pos = avio_tell(out);
avio_wb32(out, 0); // afrt size
avio_wl32(out, MKTAG('a','f','r','t'));
avio_wb32(out, 0); // version + flags
avio_wb32(out, 1000); // timescale
avio_w8(out, 0); // QualityEntryCount
avio_wb32(out, fragments); // FragmentRunEntryCount
for (i = start; i < os->nb_fragments; i++) {
avio_wb32(out, os->fragments[i]->n);
avio_wb64(out, os->fragments[i]->start_time);
avio_wb32(out, os->fragments[i]->duration);
}
update_size(out, afrt_pos);
update_size(out, 0);
avio_close(out);
rename(temp_filename, filename);
return 0;
}
static int init_file(AVFormatContext *s, OutputStream *os, int64_t start_ts)
{
int ret, i;
ret = avio_open2(&os->out, os->temp_filename, AVIO_FLAG_WRITE,
&s->interrupt_callback, NULL);
if (ret < 0)
return ret;
avio_wb32(os->out, 0);
avio_wl32(os->out, MKTAG('m','d','a','t'));
for (i = 0; i < os->nb_extra_packets; i++) {
AV_WB24(os->extra_packets[i] + 4, start_ts);
os->extra_packets[i][7] = (start_ts >> 24) & 0x7f;
avio_write(os->out, os->extra_packets[i], os->extra_packet_sizes[i]);
}
return 0;
}
static void close_file(OutputStream *os)
{
int64_t pos = avio_tell(os->out);
avio_seek(os->out, 0, SEEK_SET);
avio_wb32(os->out, pos);
avio_flush(os->out);
avio_close(os->out);
os->out = NULL;
}
static int hds_write_header(AVFormatContext *s)
{
HDSContext *c = s->priv_data;
int ret = 0, i;
AVOutputFormat *oformat;
mkdir(s->filename, 0777);
oformat = av_guess_format("flv", NULL, NULL);
if (!oformat) {
ret = AVERROR_MUXER_NOT_FOUND;
goto fail;
}
c->streams = av_mallocz(sizeof(*c->streams) * s->nb_streams);
if (!c->streams) {
ret = AVERROR(ENOMEM);
goto fail;
}
for (i = 0; i < s->nb_streams; i++) {
OutputStream *os = &c->streams[c->nb_streams];
AVFormatContext *ctx;
AVStream *st = s->streams[i];
if (!st->codec->bit_rate) {
av_log(s, AV_LOG_ERROR, "No bit rate set for stream %d\n", i);
ret = AVERROR(EINVAL);
goto fail;
}
if (st->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
if (os->has_video) {
c->nb_streams++;
os++;
}
os->has_video = 1;
} else if (st->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
if (os->has_audio) {
c->nb_streams++;
os++;
}
os->has_audio = 1;
} else {
av_log(s, AV_LOG_ERROR, "Unsupported stream type in stream %d\n", i);
ret = AVERROR(EINVAL);
goto fail;
}
os->bitrate += s->streams[i]->codec->bit_rate;
if (!os->ctx) {
os->first_stream = i;
ctx = avformat_alloc_context();
if (!ctx) {
ret = AVERROR(ENOMEM);
goto fail;
}
os->ctx = ctx;
ctx->oformat = oformat;
ctx->interrupt_callback = s->interrupt_callback;
ctx->pb = avio_alloc_context(os->iobuf, sizeof(os->iobuf),
AVIO_FLAG_WRITE, os,
NULL, hds_write, NULL);
if (!ctx->pb) {
ret = AVERROR(ENOMEM);
goto fail;
}
} else {
ctx = os->ctx;
}
s->streams[i]->id = c->nb_streams;
if (!(st = avformat_new_stream(ctx, NULL))) {
ret = AVERROR(ENOMEM);
goto fail;
}
avcodec_copy_context(st->codec, s->streams[i]->codec);
st->sample_aspect_ratio = s->streams[i]->sample_aspect_ratio;
}
if (c->streams[c->nb_streams].ctx)
c->nb_streams++;
for (i = 0; i < c->nb_streams; i++) {
OutputStream *os = &c->streams[i];
int j;
if ((ret = avformat_write_header(os->ctx, NULL)) < 0) {
goto fail;
}
os->ctx_inited = 1;
avio_flush(os->ctx->pb);
for (j = 0; j < os->ctx->nb_streams; j++)
s->streams[os->first_stream + j]->time_base = os->ctx->streams[j]->time_base;
snprintf(os->temp_filename, sizeof(os->temp_filename),
"%s/stream%d_temp", s->filename, i);
init_file(s, os, 0);
if (!os->has_video && c->min_frag_duration <= 0) {
av_log(s, AV_LOG_WARNING,
"No video stream in output stream %d and no min frag duration set\n", i);
ret = AVERROR(EINVAL);
}
os->fragment_index = 1;
write_abst(s, os, 0);
}
ret = write_manifest(s, 0);
fail:
if (ret)
hds_free(s);
return ret;
}
static int add_fragment(OutputStream *os, const char *file,
int64_t start_time, int64_t duration)
{
Fragment *frag;
if (duration == 0)
duration = 1;
if (os->nb_fragments >= os->fragments_size) {
int ret;
os->fragments_size = (os->fragments_size + 1) * 2;
if ((ret = av_reallocp_array(&os->fragments, os->fragments_size,
sizeof(*os->fragments))) < 0) {
os->fragments_size = 0;
os->nb_fragments = 0;
return ret;
}
}
frag = av_mallocz(sizeof(*frag));
if (!frag)
return AVERROR(ENOMEM);
av_strlcpy(frag->file, file, sizeof(frag->file));
frag->start_time = start_time;
frag->duration = duration;
frag->n = os->fragment_index;
os->fragments[os->nb_fragments++] = frag;
os->fragment_index++;
return 0;
}
static int hds_flush(AVFormatContext *s, OutputStream *os, int final,
int64_t end_ts)
{
HDSContext *c = s->priv_data;
int i, ret = 0;
char target_filename[1024];
int index = s->streams[os->first_stream]->id;
if (!os->packets_written)
return 0;
avio_flush(os->ctx->pb);
os->packets_written = 0;
close_file(os);
snprintf(target_filename, sizeof(target_filename),
"%s/stream%dSeg1-Frag%d", s->filename, index, os->fragment_index);
rename(os->temp_filename, target_filename);
add_fragment(os, target_filename, os->frag_start_ts, end_ts - os->frag_start_ts);
if (!final) {
ret = init_file(s, os, end_ts);
if (ret < 0)
return ret;
}
if (c->window_size || (final && c->remove_at_exit)) {
int remove = os->nb_fragments - c->window_size - c->extra_window_size;
if (final && c->remove_at_exit)
remove = os->nb_fragments;
if (remove > 0) {
for (i = 0; i < remove; i++) {
unlink(os->fragments[i]->file);
av_free(os->fragments[i]);
}
os->nb_fragments -= remove;
memmove(os->fragments, os->fragments + remove,
os->nb_fragments * sizeof(*os->fragments));
}
}
if (ret >= 0)
ret = write_abst(s, os, final);
return ret;
}
static int hds_write_packet(AVFormatContext *s, AVPacket *pkt)
{
HDSContext *c = s->priv_data;
AVStream *st = s->streams[pkt->stream_index];
OutputStream *os = &c->streams[s->streams[pkt->stream_index]->id];
int64_t end_dts = (os->fragment_index) * c->min_frag_duration;
int ret;
if (st->first_dts == AV_NOPTS_VALUE)
st->first_dts = pkt->dts;
if ((!os->has_video || st->codec->codec_type == AVMEDIA_TYPE_VIDEO) &&
av_compare_ts(pkt->dts - st->first_dts, st->time_base,
end_dts, AV_TIME_BASE_Q) >= 0 &&
pkt->flags & AV_PKT_FLAG_KEY && os->packets_written) {
if ((ret = hds_flush(s, os, 0, pkt->dts)) < 0)
return ret;
}
// Note, these fragment start timestamps, that represent a whole
// OutputStream, assume all streams in it have the same time base.
if (!os->packets_written)
os->frag_start_ts = pkt->dts;
os->last_ts = pkt->dts;
os->packets_written++;
return ff_write_chained(os->ctx, pkt->stream_index - os->first_stream, pkt, s);
}
static int hds_write_trailer(AVFormatContext *s)
{
HDSContext *c = s->priv_data;
int i;
for (i = 0; i < c->nb_streams; i++)
hds_flush(s, &c->streams[i], 1, c->streams[i].last_ts);
write_manifest(s, 1);
if (c->remove_at_exit) {
char filename[1024];
snprintf(filename, sizeof(filename), "%s/index.f4m", s->filename);
unlink(filename);
for (i = 0; i < c->nb_streams; i++) {
snprintf(filename, sizeof(filename), "%s/stream%d.abst", s->filename, i);
unlink(filename);
}
rmdir(s->filename);
}
hds_free(s);
return 0;
}
#define OFFSET(x) offsetof(HDSContext, x)
#define E AV_OPT_FLAG_ENCODING_PARAM
static const AVOption options[] = {
{ "window_size", "number of fragments kept in the manifest", OFFSET(window_size), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, E },
{ "extra_window_size", "number of fragments kept outside of the manifest before removing from disk", OFFSET(extra_window_size), AV_OPT_TYPE_INT, { .i64 = 5 }, 0, INT_MAX, E },
{ "min_frag_duration", "minimum fragment duration (in microseconds)", OFFSET(min_frag_duration), AV_OPT_TYPE_INT64, { .i64 = 10000000 }, 0, INT_MAX, E },
{ "remove_at_exit", "remove all fragments when finished", OFFSET(remove_at_exit), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, E },
{ NULL },