diff --git a/doc/ffmpeg.texi b/doc/ffmpeg.texi index e996ab945f..da37e3ad37 100644 --- a/doc/ffmpeg.texi +++ b/doc/ffmpeg.texi @@ -663,10 +663,11 @@ Not all muxers support embedded thumbnails, and those who do, only support a few Creates a program with the specified @var{title}, @var{program_num} and adds the specified @var{stream}(s) to it. -@item -stream_group type=@var{type}:st=@var{stream}[:st=@var{stream}][:stg=@var{stream_group}][:id=@var{stream_group_id}...] (@emph{output}) +@item -stream_group [map=@var{input_file_id}=@var{stream_group}][type=@var{type}:]st=@var{stream}[:st=@var{stream}][:stg=@var{stream_group}][:id=@var{stream_group_id}...] (@emph{output}) -Creates a stream group of the specified @var{type}, @var{stream_group_id} and adds the specified -@var{stream}(s) and/or previously defined @var{stream_group}(s) to it. +Creates a stream group of the specified @var{type} and @var{stream_group_id}, or by +@var{map}ping an input group, adding the specified @var{stream}(s) and/or previously +defined @var{stream_group}(s) to it. @var{type} can be one of the following: @table @option @@ -863,6 +864,27 @@ all sub-mix element's @var{annotations}s @end table +E.g. to create an scalable 5.1 IAMF file from several WAV input files +@example +ffmpeg -i front.wav -i back.wav -i center.wav -i lfe.wav +-map 0:0 -map 1:0 -map 2:0 -map 3:0 -c:a opus +-stream_group type=iamf_audio_element:id=1:st=0:st=1:st=2:st=3, +demixing=parameter_id=998, +recon_gain=parameter_id=101, +layer=ch_layout=stereo, +layer=ch_layout=5.1, +-stream_group type=iamf_mix_presentation:id=2:stg=0:annotations=en-us=Mix_Presentation, +submix=parameter_id=100:parameter_rate=48000|element=stg=0:parameter_id=100:annotations=en-us=Scalable_Submix|layout=sound_system=stereo|layout=sound_system=5.1 +-streamid 0:0 -streamid 1:1 -streamid 2:2 -streamid 3:3 output.iamf +@end example + +To copy the two stream groups (Audio Element and Mix Presentation) from an input IAMF file with four +streams into an mp4 output +@example +ffmpeg -i input.iamf -c:a copy -stream_group map=0=0:st=0:st=1:st=2:st=3 -stream_group map=0=1:stg=0 +-streamid 0:0 -streamid 1:1 -streamid 2:2 -streamid 3:3 output.mp4 +@end example + @item -target @var{type} (@emph{output}) Specify target file type (@code{vcd}, @code{svcd}, @code{dvd}, @code{dv}, @code{dv50}). @var{type} may be prefixed with @code{pal-}, @code{ntsc-} or diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c index 6d8bd5bcdf..a46b0628d8 100644 --- a/fftools/ffmpeg_mux_init.c +++ b/fftools/ffmpeg_mux_init.c @@ -2232,11 +2232,137 @@ fail: return ret; } +static int of_serialize_options(Muxer *mux, void *obj, AVBPrint *bp) +{ + char *ptr; + int ret; + + ret = av_opt_serialize(obj, 0, AV_OPT_SERIALIZE_SKIP_DEFAULTS | AV_OPT_SERIALIZE_SEARCH_CHILDREN, + &ptr, '=', ':'); + if (ret < 0) { + av_log(mux, AV_LOG_ERROR, "Failed to serialize group\n"); + return ret; + } + + av_bprintf(bp, "%s", ptr); + ret = strlen(ptr); + av_free(ptr); + + return ret; +} + +#define SERIALIZE(parent, child) do { \ + ret = of_serialize_options(mux, parent->child, bp); \ + if (ret < 0) \ + return ret; \ +} while (0) + +#define SERIALIZE_LOOP(parent, child, suffix, separator) do { \ + for (int j = 0; j < parent->nb_## child ## suffix; j++) { \ + av_bprintf(bp, separator#child "="); \ + SERIALIZE(parent, child ## suffix[j]); \ + } \ +} while (0) + +static int64_t get_stream_group_index_from_id(Muxer *mux, int64_t id) +{ + AVFormatContext *oc = mux->fc; + + for (unsigned i = 0; i < oc->nb_stream_groups; i++) + if (oc->stream_groups[i]->id == id) + return oc->stream_groups[i]->index; + + return AVERROR(EINVAL); +} + +static int of_map_group(Muxer *mux, AVDictionary **dict, AVBPrint *bp, const char *map) +{ + AVStreamGroup *stg; + int ret, file_idx, stream_idx; + char *ptr; + + file_idx = strtol(map, &ptr, 0); + if (file_idx >= nb_input_files || file_idx < 0 || map == ptr) { + av_log(mux, AV_LOG_ERROR, "Invalid input file index: %d.\n", file_idx); + return AVERROR(EINVAL); + } + + stream_idx = strtol(*ptr == '=' ? ptr + 1 : ptr, &ptr, 0); + if (*ptr || stream_idx >= input_files[file_idx]->ctx->nb_stream_groups || stream_idx < 0) { + av_log(mux, AV_LOG_ERROR, "Invalid input stream group index: %d.\n", stream_idx); + return AVERROR(EINVAL); + } + + stg = input_files[file_idx]->ctx->stream_groups[stream_idx]; + ret = of_serialize_options(mux, stg, bp); + if (ret < 0) + return ret; + + ret = av_dict_parse_string(dict, bp->str, "=", ":", 0); + if (ret < 0) + av_log(mux, AV_LOG_ERROR, "Error parsing mapped group specification %s\n", ptr); + av_dict_set_int(dict, "type", stg->type, 0); + + av_log(mux, AV_LOG_VERBOSE, "stg %s\n", bp->str); + av_bprint_clear(bp); + switch(stg->type) { + case AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT: { + AVIAMFAudioElement *audio_element = stg->params.iamf_audio_element; + + if (audio_element->demixing_info) { + av_bprintf(bp, ",demixing="); + SERIALIZE(audio_element, demixing_info); + } + if (audio_element->recon_gain_info) { + av_bprintf(bp, ",recon_gain="); + SERIALIZE(audio_element, recon_gain_info); + } + SERIALIZE_LOOP(audio_element, layer, s, ","); + break; + } + case AV_STREAM_GROUP_PARAMS_IAMF_MIX_PRESENTATION: { + AVIAMFMixPresentation *mix = stg->params.iamf_mix_presentation; + + for (int i = 0; i < mix->nb_submixes; i++) { + AVIAMFSubmix *submix = mix->submixes[i]; + + av_bprintf(bp, ",submix="); + SERIALIZE(mix, submixes[i]); + for (int j = 0; j < submix->nb_elements; j++) { + AVIAMFSubmixElement *element = submix->elements[j]; + int64_t id = get_stream_group_index_from_id(mux, element->audio_element_id); + + if (id < 0) { + av_log(mux, AV_LOG_ERROR, "Invalid or missing stream group index in" + "submix element"); + return id; + } + + av_bprintf(bp, "|element="); + SERIALIZE(submix, elements[j]); + if (ret) + av_bprintf(bp, ":"); + av_bprintf(bp, "stg=%"PRId64, id); + } + SERIALIZE_LOOP(submix, layout, s, "|"); + } + break; + } + default: + av_log(mux, AV_LOG_ERROR, "Unsupported mapped group type %d.\n", stg->type); + ret = AVERROR(EINVAL); + break; + } + av_log(mux, AV_LOG_VERBOSE, "extra %s\n", bp->str); + return 0; +} + static int of_parse_group_token(Muxer *mux, const char *token, char *ptr) { AVFormatContext *oc = mux->fc; AVStreamGroup *stg; AVDictionary *dict = NULL, *tmp = NULL; + char *mapped_string = NULL; const AVDictionaryEntry *e; const AVOption opts[] = { { "type", "Set group type", offsetof(AVStreamGroup, type), AV_OPT_TYPE_INT, @@ -2262,8 +2388,31 @@ static int of_parse_group_token(Muxer *mux, const char *token, char *ptr) return ret; } + av_dict_copy(&tmp, dict, 0); + e = av_dict_get(dict, "map", NULL, 0); + if (e) { + AVBPrint bp; + + if (ptr) { + av_log(mux, AV_LOG_ERROR, "Unexpected extra parameters when mapping a" + " stream group\n"); + ret = AVERROR(EINVAL); + goto end; + } + + av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC); + ret = of_map_group(mux, &tmp, &bp, e->value); + if (ret < 0) { + av_bprint_finalize(&bp, NULL); + goto end; + } + + av_bprint_finalize(&bp, &mapped_string); + ptr = mapped_string; + } + // "type" is not a user settable AVOption in AVStreamGroup, so handle it here - e = av_dict_get(dict, "type", NULL, 0); + e = av_dict_get(tmp, "type", NULL, 0); if (!e) { av_log(mux, AV_LOG_ERROR, "No type specified for Stream Group in \"%s\"\n", token); ret = AVERROR(EINVAL); @@ -2278,7 +2427,6 @@ static int of_parse_group_token(Muxer *mux, const char *token, char *ptr) goto end; } - av_dict_copy(&tmp, dict, 0); stg = avformat_stream_group_create(oc, type, &tmp); if (!stg) { ret = AVERROR(ENOMEM); @@ -2331,6 +2479,7 @@ static int of_parse_group_token(Muxer *mux, const char *token, char *ptr) // make sure that nothing but "st" and "stg" entries are left in the dict e = NULL; + av_dict_set(&tmp, "map", NULL, 0); av_dict_set(&tmp, "type", NULL, 0); while (e = av_dict_iterate(tmp, e)) { if (!strcmp(e->key, "st") || !strcmp(e->key, "stg")) @@ -2343,6 +2492,7 @@ static int of_parse_group_token(Muxer *mux, const char *token, char *ptr) ret = 0; end: + av_free(mapped_string); av_dict_free(&dict); av_dict_free(&tmp);