From patchwork Mon Oct 30 15:23:52 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: James Almer X-Patchwork-Id: 44439 Delivered-To: ffmpegpatchwork2@gmail.com Received: by 2002:a05:6a20:dd83:b0:15d:8365:d4b8 with SMTP id kw3csp1501373pzb; Mon, 30 Oct 2023 08:24:42 -0700 (PDT) X-Google-Smtp-Source: AGHT+IEYvAvUxw443EufCsycTQETgZWFWCLfmVQyBjU687Z2tmu7v/N+Pexxe9yvttGHkdbRty0c X-Received: by 2002:a50:9b41:0:b0:543:50cd:3f3d with SMTP id a1-20020a509b41000000b0054350cd3f3dmr968161edj.16.1698679482004; Mon, 30 Oct 2023 08:24:42 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1698679481; cv=none; d=google.com; s=arc-20160816; b=bGpG0H7lF3Mekj3yoA2Tv43vLrzI27TXWD08cZ3CFlGBfr01wfF+UCrgB8F5zjXYvh aoNUNkzKSIP2/Jg8B0Cd+fh9LCRtN0GhZA4xi04LQL5Dzi2K/0QrwfFGz/kSZTMM9NGH oi90TyEL5ciw6X6KxvjZ7AjJMh+CxXvj5XIOyATE+Wwic5C42CEb3tCdKAlrZQtRvoo+ NulE/q5PARKO9HqMvJm7d/mpB7MDURgd2yv9zqYZQ4YojpVl5dxQkP8w08mFmuJuiDIb ERgHlW6kfy+z8yZ3CvI7BGdCSdvcjhZwqEoli2Y3oSYxf+ahpRmUfLb5p8Hevfpm4iZd d8lA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=sender:errors-to:content-transfer-encoding:reply-to:list-subscribe :list-help:list-post:list-archive:list-unsubscribe:list-id :precedence:subject:mime-version:message-id:date:to:from :dkim-signature:delivered-to; bh=APXuDxZxyKk+eh/hLotZfFY33r2bU5h6JRa/SIzgGD4=; fh=YOA8vD9MJZuwZ71F/05pj6KdCjf6jQRmzLS+CATXUQk=; b=QdiGvOgFV+5Fv0xCjTIIV6b1xQUamjVX5gHbqWTV5A1xAVhUOa6B2l5uzSIbtceLE0 /FaP0qtXDJSNmYFd9h8uQk72/Vx713xGTX0rkSisPSpq+6uv04iVK6ZFrnY0B6G3PKEt vBuRXf6lQDi6MI1SEXBZglXq1s8GZe4gatTi6Ns0D3Ku33fQWFSg+LP3qk/vDK4miSlT If7vXSTI8LbNM6FfH7fqP1QweA4cWxqh5LS9O3oGUf2vx7aXjdoiD7XwQRMYh/WBep1W lpMRVK6L7ZCu8hxwalucIjVf0wkYpxwx5bAkeENR7T13DT7vkwi+nIsYskxkO0Dz1+/8 cUOQ== ARC-Authentication-Results: i=1; mx.google.com; dkim=neutral (body hash did not verify) header.i=@gmail.com header.s=20230601 header.b=be1zII3d; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org; dmarc=fail (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org. [79.124.17.100]) by mx.google.com with ESMTP id u20-20020a509514000000b0053de875ad46si3340904eda.441.2023.10.30.08.24.40; Mon, 30 Oct 2023 08:24:41 -0700 (PDT) Received-SPF: pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) client-ip=79.124.17.100; Authentication-Results: mx.google.com; dkim=neutral (body hash did not verify) header.i=@gmail.com header.s=20230601 header.b=be1zII3d; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org; dmarc=fail (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 15C5E68CC89; Mon, 30 Oct 2023 17:24:37 +0200 (EET) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from mail-yw1-f179.google.com (mail-yw1-f179.google.com [209.85.128.179]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id 5455A68C9B8 for ; Mon, 30 Oct 2023 17:24:31 +0200 (EET) Received: by mail-yw1-f179.google.com with SMTP id 00721157ae682-59b5484fbe6so41647667b3.1 for ; Mon, 30 Oct 2023 08:24:31 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1698679469; x=1699284269; darn=ffmpeg.org; h=content-transfer-encoding:mime-version:message-id:date:subject:to :from:from:to:cc:subject:date:message-id:reply-to; bh=RkW85Xy42l67aBkHLJZ/G0yEWwLWlY4QtEVorx9MQpk=; b=be1zII3dS22wdoTov/a6RfFrRy9LWtqfjZRdWI7Wt1748C5y/T1qeBcl1hOBUgovjW K67jFnpiC4l3pVtbJzc/r7VJBLXYwjkjgz0RFwheE+svu3150tKgNTkmN8pgkuvUcOZV 8BHdvF2zPZjcHJpeN+IlDN5ogTwo1q7m+VkzfelOn5eh65iT2KaP+2guh1duZmr8/ELa aAb5TuGJ0CMy5DmNA9EaQf+Ld6UYj5MJE612Xsb2LlxI0H1Zh8lFLLNYLkmhK1v8gPgi OUWxHABLDQMD3EusWe8uAhoC8hG0evGfEixyeFsjmao1upbE+G/KTAVUBA288tERL+If iFZg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1698679469; x=1699284269; h=content-transfer-encoding:mime-version:message-id:date:subject:to :from:x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=RkW85Xy42l67aBkHLJZ/G0yEWwLWlY4QtEVorx9MQpk=; b=F4p9XNgXeh+IuARFnw8Y6/MutPbTb5fP0AyNF0llzg4IYRH/kjo9AD+vbOwWo+FpVS pqmqtVcYysRS0WH2qx9HOdXqVYo43hNRvwDbrT1O1HAgxd7KmOji5gJB0p8poDiknSeP c9Ho8axdqbyfNfqL4fLBcpFzCSpPE5haQNEJiiieW1jy1SJxFwv5Ur5tTZtzHaclJ8SS gDZvGbDeU7aEoR3pZsPDPaAWoETLPV14Z+HyFw5gBWX6HtdKrCgftNi8xiuDRBmOAMSm aBMNjEiKi+/3oHzKS170YHPjKzh5tDRPshiEJpJSly74o6RtWez91HxLcpn48vpepnD+ rZiw== X-Gm-Message-State: AOJu0YwX5T9Y61v760PCFe9BYHaAFxviynA+RTsg9wDxy1n3L0OnigFb 6FEGlPzC62Ke9oJo2srn12RuGKRwLIU= X-Received: by 2002:a81:b623:0:b0:59c:678:7a32 with SMTP id u35-20020a81b623000000b0059c06787a32mr10984274ywh.29.1698679469054; Mon, 30 Oct 2023 08:24:29 -0700 (PDT) Received: from localhost.localdomain (host197.190-225-105.telecom.net.ar. [190.225.105.197]) by smtp.gmail.com with ESMTPSA id m137-20020a0dca8f000000b005a23a62a25csm4434445ywd.9.2023.10.30.08.24.19 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 30 Oct 2023 08:24:23 -0700 (PDT) From: James Almer To: ffmpeg-devel@ffmpeg.org Date: Mon, 30 Oct 2023 12:23:52 -0300 Message-ID: <20231030152354.2818-1-jamrial@gmail.com> X-Mailer: git-send-email 2.42.0 MIME-Version: 1.0 Subject: [FFmpeg-devel] [PATCH v3 1/3] [PoC]avformat: introduce AVStreamGroup X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" X-TUID: l+64kUlg46UA Signed-off-by: James Almer --- No changes since last version. I'm resending this for the IAMF demuxer. I need opinions or reviews for this. We need to get this right from the start and i don't want to push something that will afterwards be considered unoptimal libavformat/avformat.c | 31 +++++++++ libavformat/avformat.h | 146 +++++++++++++++++++++++++++++++++++++++++ libavformat/dump.c | 35 ++++++++-- libavformat/internal.h | 33 ++++++++++ libavformat/options.c | 77 ++++++++++++++++++++++ 5 files changed, 316 insertions(+), 6 deletions(-) diff --git a/libavformat/avformat.c b/libavformat/avformat.c index 5b8bb7879e..05d9837b97 100644 --- a/libavformat/avformat.c +++ b/libavformat/avformat.c @@ -80,6 +80,25 @@ FF_ENABLE_DEPRECATION_WARNINGS av_freep(pst); } +void ff_free_stream_group(AVStreamGroup **pstg) +{ + AVStreamGroup *stg = *pstg; + + if (!stg) + return; + + av_freep(&stg->streams); + av_dict_free(&stg->metadata); + av_freep(&stg->priv_data); + switch (stg->type) { + // Structs in the union are freed here + default: + break; + } + + av_freep(pstg); +} + void ff_remove_stream(AVFormatContext *s, AVStream *st) { av_assert0(s->nb_streams>0); @@ -88,6 +107,14 @@ void ff_remove_stream(AVFormatContext *s, AVStream *st) ff_free_stream(&s->streams[ --s->nb_streams ]); } +void ff_remove_stream_group(AVFormatContext *s, AVStreamGroup *stg) +{ + av_assert0(s->nb_stream_groups > 0); + av_assert0(s->stream_groups[ s->nb_stream_groups - 1 ] == stg); + + ff_free_stream_group(&s->stream_groups[ --s->nb_stream_groups ]); +} + /* XXX: suppress the packet queue */ void ff_flush_packet_queue(AVFormatContext *s) { @@ -118,6 +145,9 @@ void avformat_free_context(AVFormatContext *s) for (unsigned i = 0; i < s->nb_streams; i++) ff_free_stream(&s->streams[i]); + for (unsigned i = 0; i < s->nb_stream_groups; i++) + ff_free_stream_group(&s->stream_groups[i]); + s->nb_stream_groups = 0; s->nb_streams = 0; for (unsigned i = 0; i < s->nb_programs; i++) { @@ -138,6 +168,7 @@ void avformat_free_context(AVFormatContext *s) av_dict_free(&si->id3v2_meta); av_packet_free(&si->pkt); av_packet_free(&si->parse_pkt); + av_freep(&s->stream_groups); av_freep(&s->streams); ff_flush_packet_queue(s); av_freep(&s->url); diff --git a/libavformat/avformat.h b/libavformat/avformat.h index 9e7eca007e..9b2ee7ff14 100644 --- a/libavformat/avformat.h +++ b/libavformat/avformat.h @@ -1018,6 +1018,77 @@ typedef struct AVStream { int pts_wrap_bits; } AVStream; +enum AVStreamGroupParamsType { + AV_STREAM_GROUP_PARAMS_NONE, +}; + +typedef struct AVStreamGroup { + /** + * A class for @ref avoptions. Set on group creation. + */ + const AVClass *av_class; + + void *priv_data; + + /** + * Group index in AVFormatContext. + */ + unsigned int index; + + /** + * Group-specific group ID. + * + * decoding: set by libavformat + * encoding: set by the user, replaced by libavformat if left unset + */ + int64_t id; + + /** + * Group-specific type + * + * decoding: set by libavformat on group creation + * encoding: set by the user on group creation + */ + enum AVStreamGroupParamsType type; + + /** + * Group-specific type parameters + */ + union { + uintptr_t dummy; // Placeholder + } params; + + /** + * Metadata that applies to the whole file. + * + * - demuxing: set by libavformat in avformat_open_input() + * - muxing: may be set by the caller before avformat_write_header() + * + * Freed by libavformat in avformat_free_context(). + */ + AVDictionary *metadata; + + /** + * Number of elements in AVStreamGroup.streams. + * + * Set by avformat_stream_group_add_stream() must not be modified by any other code. + */ + unsigned int nb_streams; + + /** + * A list of streams in the group. New entries are created with + * avformat_stream_group_add_stream(). + * + * - demuxing: entries are created by libavformat in avformat_open_input(). + * If AVFMTCTX_NOHEADER is set in ctx_flags, then new entries may also + * appear in av_read_frame(). + * - muxing: entries are created by the user before avformat_write_header(). + * + * Freed by libavformat in avformat_free_context(). + */ + const AVStream **streams; +} AVStreamGroup; + struct AVCodecParserContext *av_stream_get_parser(const AVStream *s); #if FF_API_GET_END_PTS @@ -1726,6 +1797,26 @@ typedef struct AVFormatContext { * @return 0 on success, a negative AVERROR code on failure */ int (*io_close2)(struct AVFormatContext *s, AVIOContext *pb); + + /** + * Number of elements in AVFormatContext.stream_groups. + * + * Set by avformat_stream_group_create(), must not be modified by any other code. + */ + unsigned int nb_stream_groups; + + /** + * A list of all stream groups in the file. New groups are created with + * avformat_stream_group_create(), and filled with avformat_stream_group_add_stream(). + * + * - demuxing: groups may be created by libavformat in avformat_open_input(). + * If AVFMTCTX_NOHEADER is set in ctx_flags, then new groups may also + * appear in av_read_frame(). + * - muxing: groups may be created by the user before avformat_write_header(). + * + * Freed by libavformat in avformat_free_context(). + */ + AVStreamGroup **stream_groups; } AVFormatContext; /** @@ -1844,6 +1935,36 @@ const AVClass *avformat_get_class(void); */ const AVClass *av_stream_get_class(void); +/** + * Get the AVClass for AVStreamGroup. It can be used in combination with + * AV_OPT_SEARCH_FAKE_OBJ for examining options. + * + * @see av_opt_find(). + */ +const AVClass *av_stream_group_get_class(void); + +/** + * Add a new empty stream group to a media file. + * + * When demuxing, it may be called by the demuxer in read_header(). If the + * flag AVFMTCTX_NOHEADER is set in s.ctx_flags, then it may also + * be called in read_packet(). + * + * When muxing, may be called by the user before avformat_write_header(). + * + * User is required to call avformat_free_context() to clean up the allocation + * by avformat_stream_group_create(). + * + * New streams can be added to the group with avformat_stream_group_add_stream(). + * + * @param s media file handle + * + * @return newly created group or NULL on error. + * @see avformat_new_stream, avformat_stream_group_add_stream. + */ +AVStreamGroup *avformat_stream_group_create(AVFormatContext *s, + enum AVStreamGroupParamsType type); + /** * Add a new stream to a media file. * @@ -1863,6 +1984,31 @@ const AVClass *av_stream_get_class(void); */ AVStream *avformat_new_stream(AVFormatContext *s, const struct AVCodec *c); +/** + * Add an already allocated stream to a stream group. + * + * When demuxing, it may be called by the demuxer in read_header(). If the + * flag AVFMTCTX_NOHEADER is set in s.ctx_flags, then it may also + * be called in read_packet(). + * + * When muxing, may be called by the user before avformat_write_header() after + * having allocated a new group with avformat_stream_group_create() and stream with + * avformat_new_stream(). + * + * User is required to call avformat_free_context() to clean up the allocation + * by avformat_stream_group_add_stream(). + * + * @param stg stream group belonging to a media file. + * @param st stream in the media file to add to the group. + * + * @retval 0 success + * @retval AVERROR(EEXIST) the stream was already in the group + * @retval "another negative error code" legitimate errors + * + * @see avformat_new_stream, avformat_stream_group_create. + */ +int avformat_stream_group_add_stream(AVStreamGroup *stg, const AVStream *st); + #if FF_API_AVSTREAM_SIDE_DATA /** * Wrap an existing array as stream side data. diff --git a/libavformat/dump.c b/libavformat/dump.c index c0868a1bb3..7713415cae 100644 --- a/libavformat/dump.c +++ b/libavformat/dump.c @@ -509,7 +509,7 @@ static void dump_sidedata(void *ctx, const AVStream *st, const char *indent) /* "user interface" functions */ static void dump_stream_format(const AVFormatContext *ic, int i, - int index, int is_output) + int group_index, int index, int is_output) { char buf[256]; int flags = (is_output ? ic->oformat->flags : ic->iformat->flags); @@ -517,6 +517,8 @@ static void dump_stream_format(const AVFormatContext *ic, int i, const FFStream *const sti = cffstream(st); const AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL, 0); const char *separator = ic->dump_separator; + const char *group_indent = group_index >= 0 ? " " : ""; + const char *extra_indent = group_index >= 0 ? " " : " "; AVCodecContext *avctx; int ret; @@ -543,7 +545,10 @@ static void dump_stream_format(const AVFormatContext *ic, int i, avcodec_string(buf, sizeof(buf), avctx, is_output); avcodec_free_context(&avctx); - av_log(NULL, AV_LOG_INFO, " Stream #%d:%d", index, i); + av_log(NULL, AV_LOG_INFO, "%s Stream #%d", group_indent, index); + if (group_index >= 0) + av_log(NULL, AV_LOG_INFO, ":%d", group_index); + av_log(NULL, AV_LOG_INFO, ":%d", i); /* the pid is an important information, so we display it */ /* XXX: add a generic system */ @@ -621,9 +626,24 @@ static void dump_stream_format(const AVFormatContext *ic, int i, av_log(NULL, AV_LOG_INFO, " (non-diegetic)"); av_log(NULL, AV_LOG_INFO, "\n"); - dump_metadata(NULL, st->metadata, " "); + dump_metadata(NULL, st->metadata, extra_indent); - dump_sidedata(NULL, st, " "); + dump_sidedata(NULL, st, extra_indent); +} + +static void dump_stream_group(const AVFormatContext *ic, uint8_t *printed, + int i, int index, int is_output) +{ + const AVStreamGroup *stg = ic->stream_groups[i]; + char buf[512]; + int ret; + + av_log(NULL, AV_LOG_INFO, " Stream group #%d:%d:", index, i); + + switch (stg->type) { + default: + break; + } } void av_dump_format(AVFormatContext *ic, int index, @@ -699,7 +719,7 @@ void av_dump_format(AVFormatContext *ic, int index, dump_metadata(NULL, program->metadata, " "); for (k = 0; k < program->nb_stream_indexes; k++) { dump_stream_format(ic, program->stream_index[k], - index, is_output); + -1, index, is_output); printed[program->stream_index[k]] = 1; } total += program->nb_stream_indexes; @@ -708,9 +728,12 @@ void av_dump_format(AVFormatContext *ic, int index, av_log(NULL, AV_LOG_INFO, " No Program\n"); } + for (i = 0; i < ic->nb_stream_groups; i++) + dump_stream_group(ic, printed, i, index, is_output); + for (i = 0; i < ic->nb_streams; i++) if (!printed[i]) - dump_stream_format(ic, i, index, is_output); + dump_stream_format(ic, i, -1, index, is_output); av_free(printed); } diff --git a/libavformat/internal.h b/libavformat/internal.h index 7702986c9c..c6181683ef 100644 --- a/libavformat/internal.h +++ b/libavformat/internal.h @@ -202,6 +202,7 @@ typedef struct FFStream { */ AVStream pub; + AVFormatContext *fmtctx; /** * Set to 1 if the codec allows reordering, so pts can be different * from dts. @@ -427,6 +428,26 @@ static av_always_inline const FFStream *cffstream(const AVStream *st) return (const FFStream*)st; } +typedef struct FFStreamGroup { + /** + * The public context. + */ + AVStreamGroup pub; + + AVFormatContext *fmtctx; +} FFStreamGroup; + + +static av_always_inline FFStreamGroup *ffstreamgroup(AVStreamGroup *stg) +{ + return (FFStreamGroup*)stg; +} + +static av_always_inline const FFStreamGroup *cffstreamgroup(const AVStreamGroup *stg) +{ + return (const FFStreamGroup*)stg; +} + #ifdef __GNUC__ #define dynarray_add(tab, nb_ptr, elem)\ do {\ @@ -608,6 +629,18 @@ void ff_free_stream(AVStream **st); */ void ff_remove_stream(AVFormatContext *s, AVStream *st); +/** + * Frees a stream group without modifying the corresponding AVFormatContext. + * Must only be called if the latter doesn't matter or if the stream + * is not yet attached to an AVFormatContext. + */ +void ff_free_stream_group(AVStreamGroup **pstg); +/** + * Remove a stream group from its AVFormatContext and free it. + * The group must be the last stream of the AVFormatContext. + */ +void ff_remove_stream_group(AVFormatContext *s, AVStreamGroup *stg); + unsigned int ff_codec_get_tag(const AVCodecTag *tags, enum AVCodecID id); enum AVCodecID ff_codec_get_id(const AVCodecTag *tags, unsigned int tag); diff --git a/libavformat/options.c b/libavformat/options.c index 1d8c52246b..b3998bae43 100644 --- a/libavformat/options.c +++ b/libavformat/options.c @@ -271,6 +271,7 @@ AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c) if (!st->codecpar) goto fail; + sti->fmtctx = s; sti->avctx = avcodec_alloc_context3(NULL); if (!sti->avctx) goto fail; @@ -325,6 +326,82 @@ fail: return NULL; } +static const AVClass stream_group_class = { + .class_name = "AVStreamGroup", + .item_name = av_default_item_name, + .version = LIBAVUTIL_VERSION_INT, +}; + +const AVClass *av_stream_group_get_class(void) +{ + return &stream_group_class; +} + +AVStreamGroup *avformat_stream_group_create(AVFormatContext *s, + enum AVStreamGroupParamsType type) +{ + AVStreamGroup **stream_groups; + AVStreamGroup *stg; + FFStreamGroup *stgi; + + stream_groups = av_realloc_array(s->stream_groups, s->nb_stream_groups + 1, + sizeof(*stream_groups)); + if (!stream_groups) + return NULL; + s->stream_groups = stream_groups; + + stgi = av_mallocz(sizeof(*stgi)); + if (!stgi) + return NULL; + stg = &stgi->pub; + + stg->av_class = &stream_group_class; + stg->type = type; + switch (type) { + // Structs in the union are allocated here + default: + break; + } + + stgi->fmtctx = s; + stg->index = s->nb_stream_groups; + + s->stream_groups[s->nb_stream_groups++] = stg; + + return stg; +fail: + ff_free_stream_group(&stg); + return NULL; +} + +static int stream_group_add_stream(AVStreamGroup *stg, const AVStream *st) +{ + const AVStream **streams = av_realloc_array(stg->streams, stg->nb_streams + 1, + sizeof(*stg->streams)); + if (!streams) + return AVERROR(ENOMEM); + + stg->streams = streams; + stg->streams[stg->nb_streams++] = st; + + return 0; +} + +int avformat_stream_group_add_stream(AVStreamGroup *stg, const AVStream *st) +{ + const FFStreamGroup *stgi = cffstreamgroup(stg); + const FFStream *sti = cffstream(st); + + if (stgi->fmtctx != sti->fmtctx) + return AVERROR(EINVAL); + + for (int i = 0; i < stg->nb_streams; i++) + if (stg->streams[i]->index == st->index) + return AVERROR(EEXIST); + + return stream_group_add_stream(stg, st); +} + static int option_is_disposition(const AVOption *opt) { return opt->type == AV_OPT_TYPE_CONST &&