/* * Tee pseudo-muxer * Copyright (c) 2012 Nicolas George * * This file is part of FFmpeg. * * FFmpeg 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. * * FFmpeg 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 FFmpeg; if not, write to the Free Software * Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "libavutil/avutil.h" #include "libavutil/avstring.h" #include "libavutil/opt.h" #include "avformat.h" #define MAX_SLAVES 16 typedef struct { AVFormatContext *avf; AVBitStreamFilterContext **bsfs; ///< bitstream filters per stream /** map from input to output streams indexes, * disabled output streams are set to -1 */ int *stream_map; } TeeSlave; typedef struct TeeContext { const AVClass *class; unsigned nb_slaves; TeeSlave slaves[MAX_SLAVES]; } TeeContext; static const char *const slave_delim = "|"; static const char *const slave_opt_open = "["; static const char *const slave_opt_close = "]"; static const char *const slave_opt_delim = ":]"; /* must have the close too */ static const char *const slave_bsfs_spec_sep = "/"; static const AVClass tee_muxer_class = { .class_name = "Tee muxer", .item_name = av_default_item_name, .version = LIBAVUTIL_VERSION_INT, }; static int parse_slave_options(void *log, char *slave, AVDictionary **options, char **filename) { const char *p; char *key, *val; int ret; if (!strspn(slave, slave_opt_open)) { *filename = slave; return 0; } p = slave + 1; if (strspn(p, slave_opt_close)) { *filename = (char *)p + 1; return 0; } while (1) { ret = av_opt_get_key_value(&p, "=", slave_opt_delim, 0, &key, &val); if (ret < 0) { av_log(log, AV_LOG_ERROR, "No option found near \"%s\"\n", p); goto fail; } ret = av_dict_set(options, key, val, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL); if (ret < 0) goto fail; if (strspn(p, slave_opt_close)) break; p++; } *filename = (char *)p + 1; return 0; fail: av_dict_free(options); return ret; } /** * Parse list of bitstream filters and add them to the list of filters * pointed to by bsfs. * * The list must be specified in the form: * BSFS ::= BSF[,BSFS] */ static int parse_bsfs(void *log_ctx, const char *bsfs_spec, AVBitStreamFilterContext **bsfs) { char *bsf_name, *buf, *saveptr; int ret = 0; if (!(buf = av_strdup(bsfs_spec))) return AVERROR(ENOMEM); while (bsf_name = av_strtok(buf, ",", &saveptr)) { AVBitStreamFilterContext *bsf = av_bitstream_filter_init(bsf_name); if (!bsf) { av_log(log_ctx, AV_LOG_ERROR, "Cannot initialize bitstream filter with name '%s', " "unknown filter or internal error happened\n", bsf_name); ret = AVERROR_UNKNOWN; goto end; } /* append bsf context to the list of bsf contexts */ *bsfs = bsf; bsfs = &bsf->next; buf = NULL; } end: av_free(buf); return ret; } static int open_slave(AVFormatContext *avf, char *slave, TeeSlave *tee_slave) { int i, ret; AVDictionary *options = NULL; AVDictionaryEntry *entry; char *filename; char *format = NULL, *select = NULL; AVFormatContext *avf2 = NULL; AVStream *st, *st2; int stream_count; if ((ret = parse_slave_options(avf, slave, &options, &filename)) < 0) return ret; #define STEAL_OPTION(option, field) do { \ if ((entry = av_dict_get(options, option, NULL, 0))) { \ field = entry->value; \ entry->value = NULL; /* prevent it from being freed */ \ av_dict_set(&options, option, NULL, 0); \ } \ } while (0) STEAL_OPTION("f", format); STEAL_OPTION("select", select); ret = avformat_alloc_output_context2(&avf2, NULL, format, filename); if (ret < 0) goto end; tee_slave->stream_map = av_calloc(avf->nb_streams, sizeof(*tee_slave->stream_map)); if (!tee_slave->stream_map) { ret = AVERROR(ENOMEM); goto end; } stream_count = 0; for (i = 0; i < avf->nb_streams; i++) { st = avf->streams[i]; if (select) { ret = avformat_match_stream_specifier(avf, avf->streams[i], select); if (ret < 0) { av_log(avf, AV_LOG_ERROR, "Invalid stream specifier '%s' for output '%s'\n", select, slave); goto end; } if (ret == 0) { /* no match */ tee_slave->stream_map[i] = -1; continue; } } tee_slave->stream_map[i] = stream_count++; if (!(st2 = avformat_new_stream(avf2, NULL))) { ret = AVERROR(ENOMEM); goto end; } st2->id = st->id; st2->r_frame_rate = st->r_frame_rate; st2->time_base = st->time_base; st2->start_time = st->start_time; st2->duration = st->duration; st2->nb_frames = st->nb_frames; st2->disposition = st->disposition; st2->sample_aspect_ratio = st->sample_aspect_ratio; st2->avg_frame_rate = st->avg_frame_rate; av_dict_copy(&st2->metadata, st->metadata, 0); if ((ret = avcodec_copy_context(st2->codec, st->codec)) < 0) goto end; } if (!(avf2->oformat->flags & AVFMT_NOFILE)) { if ((ret = avio_open(&avf2->pb, filename, AVIO_FLAG_WRITE)) < 0) { av_log(avf, AV_LOG_ERROR, "Slave '%s': error opening: %s\n", slave, av_err2str(ret)); goto end; } } if ((ret = avformat_write_header(avf2, &options)) < 0) { av_log(avf, AV_LOG_ERROR, "Slave '%s': error writing header: %s\n", slave, av_err2str(ret)); goto end; } tee_slave->avf = avf2; tee_slave->bsfs = av_calloc(avf2->nb_streams, sizeof(TeeSlave)); if (!tee_slave->bsfs) { ret = AVERROR(ENOMEM); goto end; } entry = NULL; while (entry = av_dict_get(options, "bsfs", NULL, AV_DICT_IGNORE_SUFFIX)) { const char *spec = entry->key + strlen("bsfs"); if (*spec) { if (strspn(spec, slave_bsfs_spec_sep) != 1) { av_log(avf, AV_LOG_ERROR, "Specifier separator in '%s' is '%c', but only characters '%s' " "are allowed\n", entry->key, *spec, slave_bsfs_spec_sep); return AVERROR(EINVAL); } spec++; /* consume separator */ } for (i = 0; i < avf2->nb_streams; i++) { ret = avformat_match_stream_specifier(avf2, avf2->streams[i], spec); if (ret < 0) { av_log(avf, AV_LOG_ERROR, "Invalid stream specifier '%s' in bsfs option '%s' for slave " "output '%s'\n", spec, entry->key, filename); goto end; } if (ret > 0) { av_log(avf, AV_LOG_DEBUG, "spec:%s bsfs:%s matches stream %d of slave " "output '%s'\n", spec, entry->value, i, filename); if (tee_slave->bsfs[i]) { av_log(avf, AV_LOG_WARNING, "Duplicate bsfs specification associated to stream %d of slave " "output '%s', filters will be ignored\n", i, filename); continue; } ret = parse_bsfs(avf, entry->value, &tee_slave->bsfs[i]); if (ret < 0) { av_log(avf, AV_LOG_ERROR, "Error parsing bitstream filter sequence '%s' associated to " "stream %d of slave output '%s'\n", entry->value, i, filename); goto end; } } } av_dict_set(&options, entry->key, NULL, 0); } if (options) { entry = NULL; while ((entry = av_dict_get(options, "", entry, AV_DICT_IGNORE_SUFFIX))) av_log(avf2, AV_LOG_ERROR, "Unknown option '%s'\n", entry->key); ret = AVERROR_OPTION_NOT_FOUND; goto end; } end: av_free(format); av_dict_free(&options); return ret; } static void close_slaves(AVFormatContext *avf) { TeeContext *tee = avf->priv_data; AVFormatContext *avf2; unsigned i, j; for (i = 0; i < tee->nb_slaves; i++) { avf2 = tee->slaves[i].avf; for (j = 0; j < avf2->nb_streams; j++) { AVBitStreamFilterContext *bsf_next, *bsf = tee->slaves[i].bsfs[j]; while (bsf) { bsf_next = bsf->next; av_bitstream_filter_close(bsf); bsf = bsf_next; } } av_freep(&tee->slaves[i].stream_map); avio_close(avf2->pb); avf2->pb = NULL; avformat_free_context(avf2); tee->slaves[i].avf = NULL; } } static void log_slave(TeeSlave *slave, void *log_ctx, int log_level) { int i; av_log(log_ctx, log_level, "filename:'%s' format:%s\n", slave->avf->filename, slave->avf->oformat->name); for (i = 0; i < slave->avf->nb_streams; i++) { AVStream *st = slave->avf->streams[i]; AVBitStreamFilterContext *bsf = slave->bsfs[i]; av_log(log_ctx, log_level, " stream:%d codec:%s type:%s", i, avcodec_get_name(st->codec->codec_id), av_get_media_type_string(st->codec->codec_type)); if (bsf) { av_log(log_ctx, log_level, " bsfs:"); while (bsf) { av_log(log_ctx, log_level, "%s%s", bsf->filter->name, bsf->next ? "," : ""); bsf = bsf->next; } } av_log(log_ctx, log_level, "\n"); } } static int tee_write_header(AVFormatContext *avf) { TeeContext *tee = avf->priv_data; unsigned nb_slaves = 0, i; const char *filename = avf->filename; char *slaves[MAX_SLAVES]; int ret; while (*filename) { if (nb_slaves == MAX_SLAVES) { av_log(avf, AV_LOG_ERROR, "Maximum %d slave muxers reached.\n", MAX_SLAVES); ret = AVERROR_PATCHWELCOME; goto fail; } if (!(slaves[nb_slaves++] = av_get_token(&filename, slave_delim))) { ret = AVERROR(ENOMEM); goto fail; } if (strspn(filename, slave_delim)) filename++; } for (i = 0; i < nb_slaves; i++) { if ((ret = open_slave(avf, slaves[i], &tee->slaves[i])) < 0) goto fail; log_slave(&tee->slaves[i], avf, AV_LOG_VERBOSE); av_freep(&slaves[i]); } tee->nb_slaves = nb_slaves; for (i = 0; i < avf->nb_streams; i++) { int j, mapped = 0; for (j = 0; j < tee->nb_slaves; j++) mapped += tee->slaves[j].stream_map[i] >= 0; if (!mapped) av_log(avf, AV_LOG_WARNING, "Input stream #%d is not mapped " "to any slave.\n", i); } return 0; fail: for (i = 0; i < nb_slaves; i++) av_freep(&slaves[i]); close_slaves(avf); return ret; } static int filter_packet(void *log_ctx, AVPacket *pkt, AVFormatContext *fmt_ctx, AVBitStreamFilterContext *bsf_ctx) { AVCodecContext *enc_ctx = fmt_ctx->streams[pkt->stream_index]->codec; int ret; while (bsf_ctx) { AVPacket new_pkt = *pkt; ret = av_bitstream_filter_filter(bsf_ctx, enc_ctx, NULL, &new_pkt.data, &new_pkt.size, pkt->data, pkt->size, pkt->flags & AV_PKT_FLAG_KEY); if (ret == 0 && new_pkt.data != pkt->data && new_pkt.destruct) { if ((ret = av_copy_packet(&new_pkt, pkt)) < 0) break; ret = 1; } if (ret > 0) { av_free_packet(pkt); new_pkt.buf = av_buffer_create(new_pkt.data, new_pkt.size, av_buffer_default_free, NULL, 0); if (!new_pkt.buf) break; } *pkt = new_pkt; bsf_ctx = bsf_ctx->next; } if (ret < 0) { av_log(log_ctx, AV_LOG_ERROR, "Failed to filter bitstream with filter %s for stream %d in file '%s' with codec %s\n", bsf_ctx->filter->name, pkt->stream_index, fmt_ctx->filename, avcodec_get_name(enc_ctx->codec_id)); } return ret; } static int tee_write_trailer(AVFormatContext *avf) { TeeContext *tee = avf->priv_data; AVFormatContext *avf2; int ret_all = 0, ret; unsigned i; for (i = 0; i < tee->nb_slaves; i++) { avf2 = tee->slaves[i].avf; if ((ret = av_write_trailer(avf2)) < 0) if (!ret_all) ret_all = ret; if (!(avf2->oformat->flags & AVFMT_NOFILE)) { if ((ret = avio_close(avf2->pb)) < 0) if (!ret_all) ret_all = ret; avf2->pb = NULL; } } close_slaves(avf); return ret_all; } static int tee_write_packet(AVFormatContext *avf, AVPacket *pkt) { TeeContext *tee = avf->priv_data; AVFormatContext *avf2; AVPacket pkt2; int ret_all = 0, ret; unsigned i, s; int s2; AVRational tb, tb2; for (i = 0; i < tee->nb_slaves; i++) { avf2 = tee->slaves[i].avf; s = pkt->stream_index; s2 = tee->slaves[i].stream_map[s]; if (s2 < 0) continue; if ((ret = av_copy_packet(&pkt2, pkt)) < 0 || (ret = av_dup_packet(&pkt2))< 0) if (!ret_all) { ret = ret_all; continue; } tb = avf ->streams[s ]->time_base; tb2 = avf2->streams[s2]->time_base; pkt2.pts = av_rescale_q(pkt->pts, tb, tb2); pkt2.dts = av_rescale_q(pkt->dts, tb, tb2); pkt2.duration = av_rescale_q(pkt->duration, tb, tb2); pkt2.stream_index = s2; filter_packet(avf2, &pkt2, avf2, tee->slaves[i].bsfs[s2]); if ((ret = av_interleaved_write_frame(avf2, &pkt2)) < 0) if (!ret_all) ret_all = ret; } return ret_all; } AVOutputFormat ff_tee_muxer = { .name = "tee", .long_name = NULL_IF_CONFIG_SMALL("Multiple muxer tee"), .priv_data_size = sizeof(TeeContext), .write_header = tee_write_header, .write_trailer = tee_write_trailer, .write_packet = tee_write_packet, .priv_class = &tee_muxer_class, .flags = AVFMT_NOFILE, };