From 33ec9ef96d4220c4c846a5d3f2e95ba528b8bb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Storsj=C3=B6?= Date: Mon, 9 Jan 2012 18:27:33 +0200 Subject: [PATCH] Add a tool for creating smooth streaming manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It can also optionally split the file into individual fragments, which allows it to be served from any web server without any server side support. Signed-off-by: Martin Storsjö --- libavformat/Makefile | 2 +- tools/ismindex.c | 514 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 tools/ismindex.c diff --git a/libavformat/Makefile b/libavformat/Makefile index a19ff3c48b..45f757690d 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -354,6 +354,6 @@ SKIPHEADERS-$(CONFIG_NETWORK) += network.h rtsp.h EXAMPLES = metadata output TESTPROGS = seek -TOOLS = aviocat pktdumper probetest +TOOLS = aviocat ismindex pktdumper probetest $(SUBDIR)output-example$(EXESUF): ELIBS = -lswscale diff --git a/tools/ismindex.c b/tools/ismindex.c new file mode 100644 index 0000000000..4e14d1970f --- /dev/null +++ b/tools/ismindex.c @@ -0,0 +1,514 @@ +/* + * Copyright (c) 2012 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 + */ + +/* + * To create a simple file for smooth streaming: + * avconv -movflags frag_keyframe foo.ismv + * ismindex -n foo foo.ismv + * This step creates foo.ism and foo.ismc that is required by IIS for + * serving it. + * + * To pre-split files for serving as static files by a web server without + * any extra server support, create the ismv file as above, and split it: + * ismindex -split foo.ismv + * This step creates a file Manifest and directories QualityLevel(...), + * that can be read directly by a smooth streaming player. + */ + +#include +#include +#include +#include "libavformat/avformat.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/mathematics.h" + +static int usage(const char *argv0, int ret) +{ + fprintf(stderr, "%s [-split] [-n basename] file1 [file2] ...\n", argv0); + return ret; +} + +struct MoofOffset { + int64_t time; + int64_t offset; + int duration; +}; + +struct VideoFile { + const char *name; + int64_t duration; + int bitrate; + int track_id; + int is_audio, is_video; + int width, height; + int chunks; + int sample_rate, channels; + uint8_t *codec_private; + int codec_private_size; + struct MoofOffset *offsets; + int timescale; + const char *fourcc; + int blocksize; + int tag; +}; + +struct VideoFiles { + int nb_files; + int64_t duration; + struct VideoFile **files; + int video_file, audio_file; + int nb_video_files, nb_audio_files; +}; + +static int copy_tag(AVIOContext *in, AVIOContext *out, int32_t tag_name) +{ + int32_t size, tag; + + size = avio_rb32(in); + tag = avio_rb32(in); + avio_wb32(out, size); + avio_wb32(out, tag); + if (tag != tag_name) + return -1; + size -= 8; + while (size > 0) { + char buf[1024]; + int len = FFMIN(sizeof(buf), size); + if (avio_read(in, buf, len) != len) + break; + avio_write(out, buf, len); + size -= len; + } + return 0; +} + +static int write_fragment(const char *filename, AVIOContext *in) +{ + AVIOContext *out = NULL; + int ret; + + if ((ret = avio_open2(&out, filename, AVIO_FLAG_WRITE, NULL, NULL)) < 0) + return ret; + copy_tag(in, out, MKBETAG('m', 'o', 'o', 'f')); + copy_tag(in, out, MKBETAG('m', 'd', 'a', 't')); + + avio_flush(out); + avio_close(out); + + return ret; +} + +static int write_fragments(struct VideoFiles *files, int start_index, + AVIOContext *in) +{ + char dirname[100], filename[500]; + int i, j; + + for (i = start_index; i < files->nb_files; i++) { + struct VideoFile *vf = files->files[i]; + const char *type = vf->is_video ? "video" : "audio"; + snprintf(dirname, sizeof(dirname), "QualityLevels(%d)", vf->bitrate); + mkdir(dirname, 0777); + for (j = 0; j < vf->chunks; j++) { + snprintf(filename, sizeof(filename), "%s/Fragments(%s=%"PRId64")", + dirname, type, vf->offsets[j].time); + avio_seek(in, vf->offsets[j].offset, SEEK_SET); + write_fragment(filename, in); + } + } + return 0; +} + +static int read_tfra(struct VideoFiles *files, int start_index, AVIOContext *f) +{ + int ret = AVERROR_EOF, track_id; + int version, fieldlength, i, j; + int64_t pos = avio_tell(f); + uint32_t size = avio_rb32(f); + struct VideoFile *vf = NULL; + + if (avio_rb32(f) != MKBETAG('t', 'f', 'r', 'a')) + goto fail; + version = avio_r8(f); + avio_rb24(f); + track_id = avio_rb32(f); /* track id */ + for (i = start_index; i < files->nb_files && !vf; i++) { + if (files->files[i]->track_id == track_id) + vf = files->files[i]; + } + if (!vf) { + /* Ok, continue parsing the next atom */ + ret = 0; + goto fail; + } + fieldlength = avio_rb32(f); + vf->chunks = avio_rb32(f); + vf->offsets = av_mallocz(sizeof(*vf->offsets) * vf->chunks); + if (!vf->offsets) { + ret = AVERROR(ENOMEM); + goto fail; + } + for (i = 0; i < vf->chunks; i++) { + if (version == 1) { + vf->offsets[i].time = avio_rb64(f); + vf->offsets[i].offset = avio_rb64(f); + } else { + vf->offsets[i].time = avio_rb32(f); + vf->offsets[i].offset = avio_rb32(f); + } + for (j = 0; j < ((fieldlength >> 4) & 3) + 1; j++) + avio_r8(f); + for (j = 0; j < ((fieldlength >> 2) & 3) + 1; j++) + avio_r8(f); + for (j = 0; j < ((fieldlength >> 0) & 3) + 1; j++) + avio_r8(f); + if (i > 0) + vf->offsets[i - 1].duration = vf->offsets[i].time - + vf->offsets[i - 1].time; + } + if (vf->chunks > 0) + vf->offsets[vf->chunks - 1].duration = vf->duration - + vf->offsets[vf->chunks - 1].time; + ret = 0; +fail: + avio_seek(f, pos + size, SEEK_SET); + return ret; +} + +static int read_mfra(struct VideoFiles *files, int start_index, + const char *file, int split) +{ + int err = 0; + AVIOContext *f = NULL; + int32_t mfra_size; + + if ((err = avio_open2(&f, file, AVIO_FLAG_READ, NULL, NULL)) < 0) + goto fail; + avio_seek(f, avio_size(f) - 4, SEEK_SET); + mfra_size = avio_rb32(f); + avio_seek(f, -mfra_size, SEEK_CUR); + if (avio_rb32(f) != mfra_size) + goto fail; + if (avio_rb32(f) != MKBETAG('m', 'f', 'r', 'a')) + goto fail; + while (!read_tfra(files, start_index, f)) { + /* Empty */ + } + + if (split) + write_fragments(files, start_index, f); + +fail: + if (f) + avio_close(f); + return err; +} + +static int get_private_data(struct VideoFile *vf, AVCodecContext *codec) +{ + vf->codec_private_size = codec->extradata_size; + vf->codec_private = av_mallocz(codec->extradata_size); + if (!vf->codec_private) + return AVERROR(ENOMEM); + memcpy(vf->codec_private, codec->extradata, codec->extradata_size); + return 0; +} + +static int get_video_private_data(struct VideoFile *vf, AVCodecContext *codec) +{ + AVIOContext *io = NULL; + uint16_t sps_size, pps_size; + int err = AVERROR(EINVAL); + + if (codec->codec_id == CODEC_ID_VC1) + return get_private_data(vf, codec); + + avio_open_dyn_buf(&io); + if (codec->extradata_size < 11 || codec->extradata[0] != 1) + goto fail; + sps_size = AV_RB16(&codec->extradata[6]); + if (11 + sps_size > codec->extradata_size) + goto fail; + avio_wb32(io, 0x00000001); + avio_write(io, &codec->extradata[8], sps_size); + pps_size = AV_RB16(&codec->extradata[9 + sps_size]); + if (11 + sps_size + pps_size > codec->extradata_size) + goto fail; + avio_wb32(io, 0x00000001); + avio_write(io, &codec->extradata[11 + sps_size], pps_size); + err = 0; +fail: + vf->codec_private_size = avio_close_dyn_buf(io, &vf->codec_private); + return err; +} + +static int handle_file(struct VideoFiles *files, const char *file, int split) +{ + AVFormatContext *ctx = NULL; + int err = 0, i, orig_files = files->nb_files; + char errbuf[50], *ptr; + struct VideoFile *vf; + + err = avformat_open_input(&ctx, file, NULL, NULL); + if (err < 0) { + av_strerror(err, errbuf, sizeof(errbuf)); + fprintf(stderr, "Unable to open %s: %s\n", file, errbuf); + return 1; + } + + err = avformat_find_stream_info(ctx, NULL); + if (err < 0) { + av_strerror(err, errbuf, sizeof(errbuf)); + fprintf(stderr, "Unable to identify %s: %s\n", file, errbuf); + goto fail; + } + + if (ctx->nb_streams < 1) { + fprintf(stderr, "No streams found in %s\n", file); + goto fail; + } + if (!files->duration) + files->duration = ctx->duration; + + for (i = 0; i < ctx->nb_streams; i++) { + AVStream *st = ctx->streams[i]; + vf = av_mallocz(sizeof(*vf)); + files->files = av_realloc(files->files, + sizeof(*files->files)*(files->nb_files + 1)); + files->files[files->nb_files] = vf; + + vf->name = file; + if ((ptr = strrchr(file, '/')) != NULL) + vf->name = ptr + 1; + + vf->bitrate = st->codec->bit_rate; + vf->track_id = st->id; + vf->timescale = st->time_base.den; + vf->duration = av_rescale_rnd(ctx->duration, vf->timescale, + AV_TIME_BASE, AV_ROUND_UP); + vf->is_audio = st->codec->codec_type == AVMEDIA_TYPE_AUDIO; + vf->is_video = st->codec->codec_type == AVMEDIA_TYPE_VIDEO; + + if (!vf->is_audio && !vf->is_video) { + fprintf(stderr, "Track %d in %s is neither video nor audio, " + "skipping\n", vf->track_id, file); + av_freep(&files->files[files->nb_files]); + continue; + } + + if (vf->is_audio) { + if (files->audio_file < 0) + files->audio_file = files->nb_files; + files->nb_audio_files++; + vf->channels = st->codec->channels; + vf->sample_rate = st->codec->sample_rate; + if (st->codec->codec_id == CODEC_ID_AAC) { + vf->fourcc = "AACL"; + vf->tag = 255; + vf->blocksize = 4; + } else if (st->codec->codec_id == CODEC_ID_WMAPRO) { + vf->fourcc = "WMAP"; + vf->tag = st->codec->codec_tag; + vf->blocksize = st->codec->block_align; + } + get_private_data(vf, st->codec); + } + if (vf->is_video) { + if (files->video_file < 0) + files->video_file = files->nb_files; + files->nb_video_files++; + vf->width = st->codec->width; + vf->height = st->codec->height; + if (st->codec->codec_id == CODEC_ID_H264) + vf->fourcc = "H264"; + else if (st->codec->codec_id == CODEC_ID_VC1) + vf->fourcc = "WVC1"; + get_video_private_data(vf, st->codec); + } + + files->nb_files++; + } + + avformat_close_input(&ctx); + + read_mfra(files, orig_files, file, split); + +fail: + if (ctx) + avformat_close_input(&ctx); + return err; +} + +static void output_server_manifest(struct VideoFiles *files, + const char *basename) +{ + char filename[1000]; + FILE *out; + int i; + + snprintf(filename, sizeof(filename), "%s.ism", basename); + out = fopen(filename, "w"); + if (!out) { + perror(filename); + return; + } + fprintf(out, "\n"); + fprintf(out, "\n"); + fprintf(out, "\t\n"); + fprintf(out, "\t\t\n", basename); + fprintf(out, "\t\n"); + fprintf(out, "\t\n"); + fprintf(out, "\t\t\n"); + for (i = 0; i < files->nb_files; i++) { + struct VideoFile *vf = files->files[i]; + const char *type = vf->is_video ? "video" : "audio"; + fprintf(out, "\t\t\t<%s src=\"%s\" systemBitrate=\"%d\">\n", + type, vf->name, vf->bitrate); + fprintf(out, "\t\t\t\t\n", vf->track_id); + fprintf(out, "\t\t\t\n", type); + } + fprintf(out, "\t\t\n"); + fprintf(out, "\t\n"); + fprintf(out, "\n"); + fclose(out); +} + +static void output_client_manifest(struct VideoFiles *files, + const char *basename, int split) +{ + char filename[1000]; + FILE *out; + int i, j; + + if (split) + snprintf(filename, sizeof(filename), "Manifest"); + else + snprintf(filename, sizeof(filename), "%s.ismc", basename); + out = fopen(filename, "w"); + if (!out) { + perror(filename); + return; + } + fprintf(out, "\n"); + fprintf(out, "\n", files->duration * 10); + if (files->video_file >= 0) { + struct VideoFile *vf = files->files[files->video_file]; + int index = 0; + fprintf(out, "\t\n", + files->nb_video_files, vf->chunks); + for (i = 0; i < files->nb_files; i++) { + vf = files->files[i]; + if (!vf->is_video) + continue; + fprintf(out, "\t\tbitrate, vf->fourcc, vf->width, vf->height); + for (j = 0; j < vf->codec_private_size; j++) + fprintf(out, "%02X", vf->codec_private[j]); + fprintf(out, "\" />\n"); + index++; + } + vf = files->files[files->video_file]; + for (i = 0; i < vf->chunks; i++) + fprintf(out, "\t\t\n", i, + vf->offsets[i].duration); + fprintf(out, "\t\n"); + } + if (files->audio_file >= 0) { + struct VideoFile *vf = files->files[files->audio_file]; + int index = 0; + fprintf(out, "\t\n", + files->nb_audio_files, vf->chunks); + for (i = 0; i < files->nb_files; i++) { + vf = files->files[i]; + if (!vf->is_audio) + continue; + fprintf(out, "\t\tbitrate, vf->fourcc, vf->sample_rate, + vf->channels, vf->blocksize, vf->tag); + for (j = 0; j < vf->codec_private_size; j++) + fprintf(out, "%02X", vf->codec_private[j]); + fprintf(out, "\" />\n"); + index++; + } + vf = files->files[files->audio_file]; + for (i = 0; i < vf->chunks; i++) + fprintf(out, "\t\t\n", + i, vf->offsets[i].duration); + fprintf(out, "\t\n"); + } + fprintf(out, "\n"); + fclose(out); +} + +static void clean_files(struct VideoFiles *files) +{ + int i; + for (i = 0; i < files->nb_files; i++) { + av_freep(&files->files[i]->codec_private); + av_freep(&files->files[i]->offsets); + av_freep(&files->files[i]); + } + av_freep(&files->files); + files->nb_files = 0; +} + +int main(int argc, char **argv) +{ + const char *basename = NULL; + int split = 0, i; + struct VideoFiles vf = { 0, .video_file = -1, .audio_file = -1 }; + + av_register_all(); + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-n")) { + basename = argv[i + 1]; + i++; + } else if (!strcmp(argv[i], "-split")) { + split = 1; + } else if (argv[i][0] == '-') { + return usage(argv[0], 1); + } else { + handle_file(&vf, argv[i], split); + } + } + if (!vf.nb_files || (!basename && !split)) + return usage(argv[0], 1); + + if (!split) + output_server_manifest(&vf, basename); + output_client_manifest(&vf, basename, split); + + clean_files(&vf); + + return 0; +}