diff options
author | Martin Storsjö <martin@martin.st> | 2014-10-06 11:36:17 +0300 |
---|---|---|
committer | Martin Storsjö <martin@martin.st> | 2014-11-17 16:17:07 +0200 |
commit | fe5e6e34c05e274f98528be4f77f3c474473f977 (patch) | |
tree | 4a50df42bfe9fe05347005547a8c9c87c5105c24 | |
parent | 2ded57371abead879bcee56da5131e5fac0d17ef (diff) | |
download | ffmpeg-fe5e6e34c05e274f98528be4f77f3c474473f977.tar.gz |
lavf: Add an MPEG-DASH ISOFF segmenting muxer
This is mostly to serve as a reference example on how to segment
the output from the mp4 muxer, capable of writing the segment
list in four different ways:
- SegmentTemplate with SegmentTimeline
- SegmentTemplate with implicit segments
- SegmentList with individual files
- SegmentList with one single file per track, and byte ranges
The muxer is able to serve live content (with optional windowing)
or create a static segmented MPD.
In advanced cases, users will probably want to do the segmenting
in their own application code.
Signed-off-by: Martin Storsjö <martin@martin.st>
-rw-r--r-- | Changelog | 2 | ||||
-rwxr-xr-x | configure | 1 | ||||
-rw-r--r-- | libavformat/Makefile | 1 | ||||
-rw-r--r-- | libavformat/allformats.c | 1 | ||||
-rw-r--r-- | libavformat/dashenc.c | 773 | ||||
-rw-r--r-- | libavformat/version.h | 4 |
6 files changed, 779 insertions, 3 deletions
@@ -6,7 +6,7 @@ version <next>: - HEVC/H.265 RTP payload format (draft v6) packetizer and depacketizer - avplay now exits by default at the end of playback - XCB-based screen-grabber -- creating DASH compatible fragmented MP4 +- creating DASH compatible fragmented MP4, MPEG-DASH segmenting muxer version 11: @@ -2039,6 +2039,7 @@ avi_muxer_select="riffenc" avisynth_demuxer_deps="avisynth" avisynth_demuxer_select="riffdec" caf_demuxer_select="riffdec" +dash_muxer_select="mp4_muxer" dirac_demuxer_select="dirac_parser" dv_demuxer_select="dvprofile" dv_muxer_select="dvprofile" diff --git a/libavformat/Makefile b/libavformat/Makefile index ff887f07e2..7ed53a7605 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -91,6 +91,7 @@ OBJS-$(CONFIG_CAVSVIDEO_MUXER) += rawenc.o OBJS-$(CONFIG_CDG_DEMUXER) += cdg.o OBJS-$(CONFIG_CDXL_DEMUXER) += cdxl.o OBJS-$(CONFIG_CRC_MUXER) += crcenc.o +OBJS-$(CONFIG_DASH_MUXER) += dashenc.o isom.o OBJS-$(CONFIG_DAUD_DEMUXER) += dauddec.o OBJS-$(CONFIG_DAUD_MUXER) += daudenc.o OBJS-$(CONFIG_DFA_DEMUXER) += dfa.o diff --git a/libavformat/allformats.c b/libavformat/allformats.c index bef155ffa0..7868e3e9c4 100644 --- a/libavformat/allformats.c +++ b/libavformat/allformats.c @@ -88,6 +88,7 @@ void av_register_all(void) REGISTER_DEMUXER (CDG, cdg); REGISTER_DEMUXER (CDXL, cdxl); REGISTER_MUXER (CRC, crc); + REGISTER_MUXER (DASH, dash); REGISTER_MUXDEMUX(DAUD, daud); REGISTER_DEMUXER (DFA, dfa); REGISTER_MUXDEMUX(DIRAC, dirac); diff --git a/libavformat/dashenc.c b/libavformat/dashenc.c new file mode 100644 index 0000000000..cacaf922b8 --- /dev/null +++ b/libavformat/dashenc.c @@ -0,0 +1,773 @@ +/* + * MPEG-DASH ISO BMFF segmenter + * Copyright (c) 2014 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" +#if HAVE_UNISTD_H +#include <unistd.h> +#endif + +#include "libavutil/avstring.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/mathematics.h" +#include "libavutil/opt.h" +#include "libavutil/time_internal.h" + +#include "avc.h" +#include "avformat.h" +#include "avio_internal.h" +#include "internal.h" +#include "isom.h" +#include "os_support.h" +#include "url.h" + +typedef struct Segment { + char file[1024]; + int64_t start_pos; + int range_length, index_length; + int64_t time; + int duration; + int n; +} Segment; + +typedef struct OutputStream { + AVFormatContext *ctx; + int ctx_inited; + uint8_t iobuf[32768]; + URLContext *out; + int packets_written; + char initfile[1024]; + int64_t init_start_pos; + int init_range_length; + int nb_segments, segments_size, segment_index; + Segment **segments; + int64_t first_dts, start_dts, end_dts; + + char codec_str[100]; +} OutputStream; + +typedef struct DASHContext { + const AVClass *class; /* Class for private options. */ + int window_size; + int extra_window_size; + int min_seg_duration; + int remove_at_exit; + int use_template; + int use_timeline; + int single_file; + OutputStream *streams; + int has_video, has_audio; + int nb_segments; + int last_duration; + int total_duration; + char availability_start_time[100]; + char dirname[1024]; +} DASHContext; + +static int dash_write(void *opaque, uint8_t *buf, int buf_size) +{ + OutputStream *os = opaque; + if (os->out) + ffurl_write(os->out, buf, buf_size); + return buf_size; +} + +// RFC 6381 +static void set_codec_str(AVFormatContext *s, AVCodecContext *codec, + char *str, int size) +{ + const AVCodecTag *tags[2] = { NULL, NULL }; + uint32_t tag; + if (codec->codec_type == AVMEDIA_TYPE_VIDEO) + tags[0] = ff_codec_movvideo_tags; + else if (codec->codec_type == AVMEDIA_TYPE_AUDIO) + tags[0] = ff_codec_movaudio_tags; + else + return; + + tag = av_codec_get_tag(tags, codec->codec_id); + if (!tag) + return; + if (size < 5) + return; + + AV_WL32(str, tag); + str[4] = '\0'; + if (!strcmp(str, "mp4a") || !strcmp(str, "mp4v")) { + uint32_t oti; + tags[0] = ff_mp4_obj_type; + oti = av_codec_get_tag(tags, codec->codec_id); + if (oti) + av_strlcatf(str, size, ".%02x", oti); + else + return; + + if (tag == MKTAG('m', 'p', '4', 'a')) { + if (codec->extradata_size >= 2) { + int aot = codec->extradata[0] >> 3; + if (aot == 31) + aot = ((AV_RB16(codec->extradata) >> 5) & 0x3f) + 32; + av_strlcatf(str, size, ".%d", aot); + } + } else if (tag == MKTAG('m', 'p', '4', 'v')) { + // Unimplemented, should output ProfileLevelIndication as a decimal number + av_log(s, AV_LOG_WARNING, "Incomplete RFC 6381 codec string for mp4v\n"); + } + } else if (!strcmp(str, "avc1")) { + uint8_t *tmpbuf = NULL; + uint8_t *extradata = codec->extradata; + int extradata_size = codec->extradata_size; + if (!extradata_size) + return; + if (extradata[0] != 1) { + AVIOContext *pb; + if (avio_open_dyn_buf(&pb) < 0) + return; + if (ff_isom_write_avcc(pb, extradata, extradata_size) < 0) { + avio_close_dyn_buf(pb, &tmpbuf); + av_free(tmpbuf); + return; + } + extradata_size = avio_close_dyn_buf(pb, &extradata); + tmpbuf = extradata; + } + + if (extradata_size >= 4) + av_strlcatf(str, size, ".%02x%02x%02x", + extradata[1], extradata[2], extradata[3]); + av_free(tmpbuf); + } +} + +static void dash_free(AVFormatContext *s) +{ + DASHContext *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->ctx && os->ctx_inited) + av_write_trailer(os->ctx); + if (os->ctx && os->ctx->pb) + av_free(os->ctx->pb); + ffurl_close(os->out); + os->out = NULL; + if (os->ctx) + avformat_free_context(os->ctx); + for (j = 0; j < os->nb_segments; j++) + av_free(os->segments[j]); + av_free(os->segments); + } + av_freep(&c->streams); +} + +static void output_segment_list(OutputStream *os, AVIOContext *out, DASHContext *c) +{ + int i, start_index = 0, start_number = 1; + if (c->window_size) { + start_index = FFMAX(os->nb_segments - c->window_size, 0); + start_number = FFMAX(os->segment_index - c->window_size, 1); + } + + if (c->use_template) { + int timescale = c->use_timeline ? os->ctx->streams[0]->time_base.den : AV_TIME_BASE; + avio_printf(out, "\t\t\t\t<SegmentTemplate timescale=\"%d\" ", timescale); + if (!c->use_timeline) + avio_printf(out, "duration=\"%d\" ", c->last_duration); + avio_printf(out, "initialization=\"init-stream$RepresentationID$.m4s\" media=\"chunk-stream$RepresentationID$-$Number%%05d$.m4s\" startNumber=\"%d\">\n", c->use_timeline ? start_number : 1); + if (c->use_timeline) { + avio_printf(out, "\t\t\t\t\t<SegmentTimeline>\n"); + for (i = start_index; i < os->nb_segments; ) { + Segment *seg = os->segments[i]; + int repeat = 0; + avio_printf(out, "\t\t\t\t\t\t<S "); + if (i == start_index) + avio_printf(out, "t=\"%"PRId64"\" ", seg->time); + avio_printf(out, "d=\"%d\" ", seg->duration); + while (i + repeat + 1 < os->nb_segments && os->segments[i + repeat + 1]->duration == seg->duration) + repeat++; + if (repeat > 0) + avio_printf(out, "r=\"%d\" ", repeat); + avio_printf(out, "/>\n"); + i += 1 + repeat; + } + avio_printf(out, "\t\t\t\t\t</SegmentTimeline>\n"); + } + avio_printf(out, "\t\t\t\t</SegmentTemplate>\n"); + } else if (c->single_file) { + avio_printf(out, "\t\t\t\t<BaseURL>%s</BaseURL>\n", os->initfile); + avio_printf(out, "\t\t\t\t<SegmentList timescale=\"%d\" duration=\"%d\" startNumber=\"%d\">\n", AV_TIME_BASE, c->last_duration, start_number); + avio_printf(out, "\t\t\t\t\t<Initialization range=\"%"PRId64"-%"PRId64"\" />\n", os->init_start_pos, os->init_start_pos + os->init_range_length - 1); + for (i = start_index; i < os->nb_segments; i++) { + Segment *seg = os->segments[i]; + avio_printf(out, "\t\t\t\t\t<SegmentURL mediaRange=\"%"PRId64"-%"PRId64"\" ", seg->start_pos, seg->start_pos + seg->range_length - 1); + if (seg->index_length) + avio_printf(out, "indexRange=\"%"PRId64"-%"PRId64"\" ", seg->start_pos, seg->start_pos + seg->index_length - 1); + avio_printf(out, "/>\n"); + } + avio_printf(out, "\t\t\t\t</SegmentList>\n"); + } else { + avio_printf(out, "\t\t\t\t<SegmentList timescale=\"%d\" duration=\"%d\" startNumber=\"%d\">\n", AV_TIME_BASE, c->last_duration, start_number); + avio_printf(out, "\t\t\t\t\t<Initialization sourceURL=\"%s\" />\n", os->initfile); + for (i = start_index; i < os->nb_segments; i++) { + Segment *seg = os->segments[i]; + avio_printf(out, "\t\t\t\t\t<SegmentURL media=\"%s\" />\n", seg->file); + } + avio_printf(out, "\t\t\t\t</SegmentList>\n"); + } +} + +static char *xmlescape(const char *str) { + int outlen = strlen(str)*3/2 + 6; + char *out = av_realloc(NULL, outlen + 1); + int pos = 0; + if (!out) + return NULL; + for (; *str; str++) { + if (pos + 6 > outlen) { + char *tmp; + outlen = 2 * outlen + 6; + tmp = av_realloc(out, outlen + 1); + if (!tmp) { + av_free(out); + return NULL; + } + out = tmp; + } + if (*str == '&') { + memcpy(&out[pos], "&", 5); + pos += 5; + } else if (*str == '<') { + memcpy(&out[pos], "<", 4); + pos += 4; + } else if (*str == '>') { + memcpy(&out[pos], ">", 4); + pos += 4; + } else if (*str == '\'') { + memcpy(&out[pos], "'", 6); + pos += 6; + } else if (*str == '\"') { + memcpy(&out[pos], """, 6); + pos += 6; + } else { + out[pos++] = *str; + } + } + out[pos] = '\0'; + return out; +} + +static void write_time(AVIOContext *out, int64_t time) +{ + int seconds = time / AV_TIME_BASE; + int fractions = time % AV_TIME_BASE; + int minutes = seconds / 60; + int hours = minutes / 60; + seconds %= 60; + minutes %= 60; + avio_printf(out, "PT"); + if (hours) + avio_printf(out, "%dH", hours); + if (hours || minutes) + avio_printf(out, "%dM", minutes); + avio_printf(out, "%d.%dS", seconds, fractions / (AV_TIME_BASE / 10)); +} + +static int write_manifest(AVFormatContext *s, int final) +{ + DASHContext *c = s->priv_data; + AVIOContext *out; + char temp_filename[1024]; + int ret, i; + AVDictionaryEntry *title = av_dict_get(s->metadata, "title", NULL, 0); + + snprintf(temp_filename, sizeof(temp_filename), "%s.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", temp_filename); + return ret; + } + avio_printf(out, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); + avio_printf(out, "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + "\txmlns=\"urn:mpeg:dash:schema:mpd:2011\"\n" + "\txmlns:xlink=\"http://www.w3.org/1999/xlink\"\n" + "\txsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\"\n" + "\tprofiles=\"urn:mpeg:dash:profile:isoff-live:2011\"\n" + "\ttype=\"%s\"\n", final ? "static" : "dynamic"); + if (final) { + avio_printf(out, "\tmediaPresentationDuration=\""); + write_time(out, c->total_duration); + avio_printf(out, "\"\n"); + } else { + int update_period = c->last_duration / AV_TIME_BASE; + if (c->use_template && !c->use_timeline) + update_period = 500; + avio_printf(out, "\tminimumUpdatePeriod=\"PT%dS\"\n", update_period); + avio_printf(out, "\tsuggestedPresentationDelay=\"PT%dS\"\n", c->last_duration / AV_TIME_BASE); + if (!c->availability_start_time[0] && s->nb_streams > 0 && c->streams[0].nb_segments > 0) { + time_t t = time(NULL); + struct tm *ptm, tmbuf; + ptm = gmtime_r(&t, &tmbuf); + if (ptm) { + if (!strftime(c->availability_start_time, sizeof(c->availability_start_time), + "%Y-%m-%dT%H:%M:%S", ptm)) + c->availability_start_time[0] = '\0'; + } + } + if (c->availability_start_time[0]) + avio_printf(out, "\tavailabilityStartTime=\"%s\"\n", c->availability_start_time); + if (c->window_size && c->use_template) { + avio_printf(out, "\ttimeShiftBufferDepth=\""); + write_time(out, c->last_duration * c->window_size); + avio_printf(out, "\"\n"); + } + } + avio_printf(out, "\tminBufferTime=\""); + write_time(out, c->last_duration); + avio_printf(out, "\">\n"); + avio_printf(out, "\t<ProgramInformation>\n"); + if (title) { + char *escaped = xmlescape(title->value); + avio_printf(out, "\t\t<Title>%s</Title>\n", escaped); + av_free(escaped); + } + avio_printf(out, "\t</ProgramInformation>\n"); + if (c->window_size && s->nb_streams > 0 && c->streams[0].nb_segments > 0 && !c->use_template) { + OutputStream *os = &c->streams[0]; + int start_index = FFMAX(os->nb_segments - c->window_size, 0); + int64_t start_time = av_rescale_q(os->segments[start_index]->time, s->streams[0]->time_base, AV_TIME_BASE_Q); + avio_printf(out, "\t<Period start=\""); + write_time(out, start_time); + avio_printf(out, "\">\n"); + } else { + avio_printf(out, "\t<Period start=\"PT0.0S\">\n"); + } + + if (c->has_video) { + avio_printf(out, "\t\t<AdaptationSet id=\"video\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">\n"); + for (i = 0; i < s->nb_streams; i++) { + AVStream *st = s->streams[i]; + OutputStream *os = &c->streams[i]; + if (s->streams[i]->codec->codec_type != AVMEDIA_TYPE_VIDEO) + continue; + avio_printf(out, "\t\t\t<Representation id=\"%d\" mimeType=\"video/mp4\" codecs=\"%s\" bandwidth=\"%d\" width=\"%d\" height=\"%d\">\n", i, os->codec_str, st->codec->bit_rate, st->codec->width, st->codec->height); + output_segment_list(&c->streams[i], out, c); + avio_printf(out, "\t\t\t</Representation>\n"); + } + avio_printf(out, "\t\t</AdaptationSet>\n"); + } + if (c->has_audio) { + avio_printf(out, "\t\t<AdaptationSet id=\"audio\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">\n"); + for (i = 0; i < s->nb_streams; i++) { + AVStream *st = s->streams[i]; + OutputStream *os = &c->streams[i]; + if (s->streams[i]->codec->codec_type != AVMEDIA_TYPE_AUDIO) + continue; + avio_printf(out, "\t\t\t<Representation id=\"%d\" mimeType=\"audio/mp4\" codecs=\"%s\" bandwidth=\"%d\" audioSamplingRate=\"%d\">\n", i, os->codec_str, st->codec->bit_rate, st->codec->sample_rate); + avio_printf(out, "\t\t\t\t<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"%d\" />\n", st->codec->channels); + output_segment_list(&c->streams[i], out, c); + avio_printf(out, "\t\t\t</Representation>\n"); + } + avio_printf(out, "\t\t</AdaptationSet>\n"); + } + avio_printf(out, "\t</Period>\n"); + avio_printf(out, "</MPD>\n"); + avio_flush(out); + avio_close(out); + return ff_rename(temp_filename, s->filename); +} + +static int dash_write_header(AVFormatContext *s) +{ + DASHContext *c = s->priv_data; + int ret = 0, i; + AVOutputFormat *oformat; + char *ptr; + char basename[1024]; + + if (c->single_file) + c->use_template = 0; + + av_strlcpy(c->dirname, s->filename, sizeof(c->dirname)); + ptr = strrchr(c->dirname, '/'); + if (ptr) { + av_strlcpy(basename, &ptr[1], sizeof(basename)); + ptr[1] = '\0'; + } else { + c->dirname[0] = '\0'; + av_strlcpy(basename, s->filename, sizeof(basename)); + } + + ptr = strrchr(basename, '.'); + if (ptr) + *ptr = '\0'; + + oformat = av_guess_format("mp4", 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[i]; + AVFormatContext *ctx; + AVStream *st; + AVDictionary *opts = NULL; + char filename[1024]; + + if (!s->streams[i]->codec->bit_rate) { + av_log(s, AV_LOG_ERROR, "No bit rate set for stream %d\n", i); + ret = AVERROR(EINVAL); + goto fail; + } + + ctx = avformat_alloc_context(); + if (!ctx) { + ret = AVERROR(ENOMEM); + goto fail; + } + os->ctx = ctx; + ctx->oformat = oformat; + ctx->interrupt_callback = s->interrupt_callback; + + 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; + st->time_base = s->streams[i]->time_base; + ctx->avoid_negative_ts = s->avoid_negative_ts; + + ctx->pb = avio_alloc_context(os->iobuf, sizeof(os->iobuf), AVIO_FLAG_WRITE, os, NULL, dash_write, NULL); + if (!ctx->pb) { + ret = AVERROR(ENOMEM); + goto fail; + } + + if (c->single_file) + snprintf(os->initfile, sizeof(os->initfile), "%s-stream%d.m4s", basename, i); + else + snprintf(os->initfile, sizeof(os->initfile), "init-stream%d.m4s", i); + snprintf(filename, sizeof(filename), "%s%s", c->dirname, os->initfile); + ret = ffurl_open(&os->out, filename, AVIO_FLAG_WRITE, &s->interrupt_callback, NULL); + if (ret < 0) + goto fail; + os->init_start_pos = 0; + + av_dict_set(&opts, "movflags", "frag_custom+dash", 0); + if ((ret = avformat_write_header(ctx, &opts)) < 0) { + goto fail; + } + os->ctx_inited = 1; + avio_flush(ctx->pb); + av_dict_free(&opts); + + if (c->single_file) { + os->init_range_length = avio_tell(ctx->pb); + } else { + ffurl_close(os->out); + os->out = NULL; + } + + s->streams[i]->time_base = st->time_base; + // If the muxer wants to shift timestamps, request to have them shifted + // already before being handed to this muxer, so we don't have mismatches + // between the MPD and the actual segments. + s->avoid_negative_ts = ctx->avoid_negative_ts; + if (st->codec->codec_type == AVMEDIA_TYPE_VIDEO) + c->has_video = 1; + else if (st->codec->codec_type == AVMEDIA_TYPE_AUDIO) + c->has_audio = 1; + + set_codec_str(s, os->ctx->streams[0]->codec, os->codec_str, sizeof(os->codec_str)); + os->first_dts = AV_NOPTS_VALUE; + os->segment_index = 1; + } + + if (!c->has_video && c->min_seg_duration <= 0) { + av_log(s, AV_LOG_WARNING, "no video stream and no min seg duration set\n"); + ret = AVERROR(EINVAL); + } + ret = write_manifest(s, 0); + +fail: + if (ret) + dash_free(s); + return ret; +} + +static int add_segment(OutputStream *os, const char *file, + int64_t time, int duration, + int64_t start_pos, int64_t range_length, + int64_t index_length) +{ + int err; + Segment *seg; + if (os->nb_segments >= os->segments_size) { + os->segments_size = (os->segments_size + 1) * 2; + if ((err = av_reallocp(&os->segments, sizeof(*os->segments) * + os->segments_size)) < 0) { + os->segments_size = 0; + os->nb_segments = 0; + return err; + } + } + seg = av_mallocz(sizeof(*seg)); + if (!seg) + return AVERROR(ENOMEM); + av_strlcpy(seg->file, file, sizeof(seg->file)); + seg->time = time; + seg->duration = duration; + seg->start_pos = start_pos; + seg->range_length = range_length; + seg->index_length = index_length; + os->segments[os->nb_segments++] = seg; + os->segment_index++; + return 0; +} + +static void write_styp(AVIOContext *pb) +{ + avio_wb32(pb, 24); + ffio_wfourcc(pb, "styp"); + ffio_wfourcc(pb, "msdh"); + avio_wb32(pb, 0); /* minor */ + ffio_wfourcc(pb, "msdh"); + ffio_wfourcc(pb, "msix"); +} + +static void find_index_range(AVFormatContext *s, const char *dirname, + const char *filename, int64_t pos, + int *index_length) +{ + char full_path[1024]; + uint8_t buf[8]; + URLContext *fd; + int ret; + + snprintf(full_path, sizeof(full_path), "%s%s", dirname, filename); + ret = ffurl_open(&fd, full_path, AVIO_FLAG_READ, &s->interrupt_callback, NULL); + if (ret < 0) + return; + if (ffurl_seek(fd, pos, SEEK_SET) != pos) { + ffurl_close(fd); + return; + } + ret = ffurl_read(fd, buf, 8); + ffurl_close(fd); + if (ret < 8) + return; + if (AV_RL32(&buf[4]) != MKTAG('s', 'i', 'd', 'x')) + return; + *index_length = AV_RB32(&buf[0]); +} + +static int dash_flush(AVFormatContext *s, int final) +{ + DASHContext *c = s->priv_data; + int i, ret = 0; + + for (i = 0; i < s->nb_streams; i++) { + OutputStream *os = &c->streams[i]; + char filename[1024] = "", full_path[1024], temp_path[1024]; + int64_t start_pos = avio_tell(os->ctx->pb); + int range_length, index_length = 0; + + if (!os->packets_written) + continue; + + if (!c->single_file) { + snprintf(filename, sizeof(filename), "chunk-stream%d-%05d.m4s", i, os->segment_index); + snprintf(full_path, sizeof(full_path), "%s%s", c->dirname, filename); + snprintf(temp_path, sizeof(temp_path), "%s.tmp", full_path); + ret = ffurl_open(&os->out, temp_path, AVIO_FLAG_WRITE, &s->interrupt_callback, NULL); + if (ret < 0) + break; + write_styp(os->ctx->pb); + } + av_write_frame(os->ctx, NULL); + avio_flush(os->ctx->pb); + os->packets_written = 0; + + range_length = avio_tell(os->ctx->pb) - start_pos; + if (c->single_file) { + find_index_range(s, c->dirname, os->initfile, start_pos, &index_length); + } else { + ffurl_close(os->out); + os->out = NULL; + ret = ff_rename(temp_path, full_path); + if (ret < 0) + break; + } + add_segment(os, filename, os->start_dts, os->end_dts - os->start_dts, start_pos, range_length, index_length); + } + + if (c->window_size || (final && c->remove_at_exit)) { + for (i = 0; i < s->nb_streams; i++) { + OutputStream *os = &c->streams[i]; + int j; + int remove = os->nb_segments - c->window_size - c->extra_window_size; + if (final && c->remove_at_exit) + remove = os->nb_segments; + if (remove > 0) { + for (j = 0; j < remove; j++) { + char filename[1024]; + snprintf(filename, sizeof(filename), "%s%s", c->dirname, os->segments[j]->file); + unlink(filename); + av_free(os->segments[j]); + } + os->nb_segments -= remove; + memmove(os->segments, os->segments + remove, os->nb_segments * sizeof(*os->segments)); + } + } + } + + if (ret >= 0) + ret = write_manifest(s, final); + return ret; +} + +static int dash_write_packet(AVFormatContext *s, AVPacket *pkt) +{ + DASHContext *c = s->priv_data; + AVStream *st = s->streams[pkt->stream_index]; + OutputStream *os = &c->streams[pkt->stream_index]; + int64_t seg_end_duration = (c->nb_segments + 1) * (int64_t) c->min_seg_duration; + int ret; + + // If forcing the stream to start at 0, the mp4 muxer will set the start + // timestamps to 0. Do the same here, to avoid mismatches in duration/timestamps. + if (os->first_dts == AV_NOPTS_VALUE && + s->avoid_negative_ts == AVFMT_AVOID_NEG_TS_MAKE_ZERO) { + pkt->pts -= pkt->dts; + pkt->dts = 0; + } + + if (os->first_dts == AV_NOPTS_VALUE) + os->first_dts = pkt->dts; + + if ((!c->has_video || st->codec->codec_type == AVMEDIA_TYPE_VIDEO) && + pkt->flags & AV_PKT_FLAG_KEY && os->packets_written && + av_compare_ts(pkt->dts - os->first_dts, st->time_base, + seg_end_duration, AV_TIME_BASE_Q) >= 0) { + int64_t prev_duration = c->last_duration; + + c->last_duration = av_rescale_q(pkt->dts - os->start_dts, + st->time_base, + AV_TIME_BASE_Q); + c->total_duration = av_rescale_q(pkt->dts - os->first_dts, + st->time_base, + AV_TIME_BASE_Q); + + if ((!c->use_timeline || !c->use_template) && prev_duration) { + if (c->last_duration < prev_duration*9/10 || + c->last_duration > prev_duration*11/10) { + av_log(s, AV_LOG_WARNING, + "Segment durations differ too much, enable use_timeline " + "and use_template, or keep a stricter keyframe interval\n"); + } + } + + if ((ret = dash_flush(s, 0)) < 0) + return ret; + c->nb_segments++; + } + + if (!os->packets_written) + os->start_dts = pkt->dts; + os->end_dts = pkt->dts + pkt->duration; + os->packets_written++; + return ff_write_chained(os->ctx, 0, pkt, s); +} + +static int dash_write_trailer(AVFormatContext *s) +{ + DASHContext *c = s->priv_data; + + if (s->nb_streams > 0) { + OutputStream *os = &c->streams[0]; + // If no segments have been written so far, try to do a crude + // guess of the segment duration + if (!c->last_duration) + c->last_duration = av_rescale_q(os->end_dts - os->start_dts, + s->streams[0]->time_base, + AV_TIME_BASE_Q); + c->total_duration = av_rescale_q(os->end_dts - os->first_dts, + s->streams[0]->time_base, + AV_TIME_BASE_Q); + } + dash_flush(s, 1); + + if (c->remove_at_exit) { + char filename[1024]; + int i; + for (i = 0; i < s->nb_streams; i++) { + OutputStream *os = &c->streams[i]; + snprintf(filename, sizeof(filename), "%s%s", c->dirname, os->initfile); + unlink(filename); + } + unlink(s->filename); + } + + dash_free(s); + return 0; +} + +#define OFFSET(x) offsetof(DASHContext, x) +#define E AV_OPT_FLAG_ENCODING_PARAM +static const AVOption options[] = { + { "window_size", "number of segments kept in the manifest", OFFSET(window_size), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, E }, + { "extra_window_size", "number of segments kept outside of the manifest before removing from disk", OFFSET(extra_window_size), AV_OPT_TYPE_INT, { .i64 = 5 }, 0, INT_MAX, E }, + { "min_seg_duration", "minimum segment duration (in microseconds)", OFFSET(min_seg_duration), AV_OPT_TYPE_INT64, { .i64 = 5000000 }, 0, INT_MAX, E }, + { "remove_at_exit", "remove all segments when finished", OFFSET(remove_at_exit), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, E }, + { "use_template", "Use SegmentTemplate instead of SegmentList", OFFSET(use_template), AV_OPT_TYPE_INT, { .i64 = 1 }, 0, 1, E }, + { "use_timeline", "Use SegmentTimeline in SegmentTemplate", OFFSET(use_timeline), AV_OPT_TYPE_INT, { .i64 = 1 }, 0, 1, E }, + { "single_file", "Store all segments in one file, accessed using byte ranges", OFFSET(single_file), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, E }, + { NULL }, +}; + +static const AVClass dash_class = { + .class_name = "dash muxer", + .item_name = av_default_item_name, + .option = options, + .version = LIBAVUTIL_VERSION_INT, +}; + +AVOutputFormat ff_dash_muxer = { + .name = "dash", + .long_name = NULL_IF_CONFIG_SMALL("DASH Muxer"), + .priv_data_size = sizeof(DASHContext), + .audio_codec = AV_CODEC_ID_AAC, + .video_codec = AV_CODEC_ID_H264, + .flags = AVFMT_GLOBALHEADER | AVFMT_NOFILE | AVFMT_TS_NEGATIVE, + .write_header = dash_write_header, + .write_packet = dash_write_packet, + .write_trailer = dash_write_trailer, + .codec_tag = (const AVCodecTag* const []){ ff_mp4_obj_type, 0 }, + .priv_class = &dash_class, +}; diff --git a/libavformat/version.h b/libavformat/version.h index c10a6b87c2..66927773ba 100644 --- a/libavformat/version.h +++ b/libavformat/version.h @@ -30,8 +30,8 @@ #include "libavutil/version.h" #define LIBAVFORMAT_VERSION_MAJOR 56 -#define LIBAVFORMAT_VERSION_MINOR 6 -#define LIBAVFORMAT_VERSION_MICRO 5 +#define LIBAVFORMAT_VERSION_MINOR 7 +#define LIBAVFORMAT_VERSION_MICRO 0 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ LIBAVFORMAT_VERSION_MINOR, \ |