ffmpeg: add support for muxing AVStreamGroups
Starting with IAMF support. Signed-off-by: James Almer <jamrial@gmail.com>
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
#include "libavutil/dict.h"
|
||||
#include "libavutil/display.h"
|
||||
#include "libavutil/getenv_utf8.h"
|
||||
#include "libavutil/iamf.h"
|
||||
#include "libavutil/intreadwrite.h"
|
||||
#include "libavutil/log.h"
|
||||
#include "libavutil/mem.h"
|
||||
@@ -2014,6 +2015,343 @@ static int setup_sync_queues(Muxer *mux, AVFormatContext *oc, int64_t buf_size_u
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int of_parse_iamf_audio_element_layers(Muxer *mux, AVStreamGroup *stg, char *ptr)
|
||||
{
|
||||
AVIAMFAudioElement *audio_element = stg->params.iamf_audio_element;
|
||||
AVDictionary *dict = NULL;
|
||||
const char *token;
|
||||
int ret = 0;
|
||||
|
||||
audio_element->demixing_info =
|
||||
av_iamf_param_definition_alloc(AV_IAMF_PARAMETER_DEFINITION_DEMIXING, 1, NULL);
|
||||
audio_element->recon_gain_info =
|
||||
av_iamf_param_definition_alloc(AV_IAMF_PARAMETER_DEFINITION_RECON_GAIN, 1, NULL);
|
||||
|
||||
if (!audio_element->demixing_info ||
|
||||
!audio_element->recon_gain_info)
|
||||
return AVERROR(ENOMEM);
|
||||
|
||||
/* process manually set layers and parameters */
|
||||
token = av_strtok(NULL, ",", &ptr);
|
||||
while (token) {
|
||||
const AVDictionaryEntry *e;
|
||||
int demixing = 0, recon_gain = 0;
|
||||
int layer = 0;
|
||||
|
||||
if (av_strstart(token, "layer=", &token))
|
||||
layer = 1;
|
||||
else if (av_strstart(token, "demixing=", &token))
|
||||
demixing = 1;
|
||||
else if (av_strstart(token, "recon_gain=", &token))
|
||||
recon_gain = 1;
|
||||
|
||||
av_dict_free(&dict);
|
||||
ret = av_dict_parse_string(&dict, token, "=", ":", 0);
|
||||
if (ret < 0) {
|
||||
av_log(mux, AV_LOG_ERROR, "Error parsing audio element specification %s\n", token);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (layer) {
|
||||
AVIAMFLayer *audio_layer = av_iamf_audio_element_add_layer(audio_element);
|
||||
if (!audio_layer) {
|
||||
av_log(mux, AV_LOG_ERROR, "Error adding layer to stream group %d\n", stg->index);
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto fail;
|
||||
}
|
||||
av_opt_set_dict(audio_layer, &dict);
|
||||
} else if (demixing || recon_gain) {
|
||||
AVIAMFParamDefinition *param = demixing ? audio_element->demixing_info
|
||||
: audio_element->recon_gain_info;
|
||||
void *subblock = av_iamf_param_definition_get_subblock(param, 0);
|
||||
|
||||
av_opt_set_dict(param, &dict);
|
||||
av_opt_set_dict(subblock, &dict);
|
||||
}
|
||||
|
||||
// make sure that no entries are left in the dict
|
||||
e = NULL;
|
||||
if (e = av_dict_iterate(dict, e)) {
|
||||
av_log(mux, AV_LOG_FATAL, "Unknown layer key %s.\n", e->key);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto fail;
|
||||
}
|
||||
token = av_strtok(NULL, ",", &ptr);
|
||||
}
|
||||
|
||||
fail:
|
||||
av_dict_free(&dict);
|
||||
if (!ret && !audio_element->nb_layers) {
|
||||
av_log(mux, AV_LOG_ERROR, "No layer in audio element specification\n");
|
||||
ret = AVERROR(EINVAL);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int of_parse_iamf_submixes(Muxer *mux, AVStreamGroup *stg, char *ptr)
|
||||
{
|
||||
AVFormatContext *oc = mux->fc;
|
||||
AVIAMFMixPresentation *mix = stg->params.iamf_mix_presentation;
|
||||
AVDictionary *dict = NULL;
|
||||
const char *token;
|
||||
char *submix_str = NULL;
|
||||
int ret = 0;
|
||||
|
||||
/* process manually set submixes */
|
||||
token = av_strtok(NULL, ",", &ptr);
|
||||
while (token) {
|
||||
AVIAMFSubmix *submix = NULL;
|
||||
const char *subtoken;
|
||||
char *subptr = NULL;
|
||||
|
||||
if (!av_strstart(token, "submix=", &token)) {
|
||||
av_log(mux, AV_LOG_ERROR, "No submix in mix presentation specification \"%s\"\n", token);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
submix_str = av_strdup(token);
|
||||
if (!submix_str)
|
||||
goto fail;
|
||||
|
||||
submix = av_iamf_mix_presentation_add_submix(mix);
|
||||
if (!submix) {
|
||||
av_log(mux, AV_LOG_ERROR, "Error adding submix to stream group %d\n", stg->index);
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto fail;
|
||||
}
|
||||
submix->output_mix_config =
|
||||
av_iamf_param_definition_alloc(AV_IAMF_PARAMETER_DEFINITION_MIX_GAIN, 0, NULL);
|
||||
if (!submix->output_mix_config) {
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
subptr = NULL;
|
||||
subtoken = av_strtok(submix_str, "|", &subptr);
|
||||
while (subtoken) {
|
||||
const AVDictionaryEntry *e;
|
||||
int element = 0, layout = 0;
|
||||
|
||||
if (av_strstart(subtoken, "element=", &subtoken))
|
||||
element = 1;
|
||||
else if (av_strstart(subtoken, "layout=", &subtoken))
|
||||
layout = 1;
|
||||
|
||||
av_dict_free(&dict);
|
||||
ret = av_dict_parse_string(&dict, subtoken, "=", ":", 0);
|
||||
if (ret < 0) {
|
||||
av_log(mux, AV_LOG_ERROR, "Error parsing submix specification \"%s\"\n", subtoken);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
AVIAMFSubmixElement *submix_element;
|
||||
int64_t idx = -1;
|
||||
|
||||
if (e = av_dict_get(dict, "stg", NULL, 0))
|
||||
idx = strtol(e->value, NULL, 0);
|
||||
av_dict_set(&dict, "stg", NULL, 0);
|
||||
if (idx < 0 || idx >= oc->nb_stream_groups - 1 ||
|
||||
oc->stream_groups[idx]->type != AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT) {
|
||||
av_log(mux, AV_LOG_ERROR, "Invalid or missing stream group index in "
|
||||
"submix element specification \"%s\"\n", subtoken);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto fail;
|
||||
}
|
||||
submix_element = av_iamf_submix_add_element(submix);
|
||||
if (!submix_element) {
|
||||
av_log(mux, AV_LOG_ERROR, "Error adding element to submix\n");
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
submix_element->audio_element_id = oc->stream_groups[idx]->id;
|
||||
|
||||
submix_element->element_mix_config =
|
||||
av_iamf_param_definition_alloc(AV_IAMF_PARAMETER_DEFINITION_MIX_GAIN, 0, NULL);
|
||||
if (!submix_element->element_mix_config)
|
||||
ret = AVERROR(ENOMEM);
|
||||
av_opt_set_dict2(submix_element, &dict, AV_OPT_SEARCH_CHILDREN);
|
||||
} else if (layout) {
|
||||
AVIAMFSubmixLayout *submix_layout = av_iamf_submix_add_layout(submix);
|
||||
if (!submix_layout) {
|
||||
av_log(mux, AV_LOG_ERROR, "Error adding layout to submix\n");
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto fail;
|
||||
}
|
||||
av_opt_set_dict(submix_layout, &dict);
|
||||
} else
|
||||
av_opt_set_dict2(submix, &dict, AV_OPT_SEARCH_CHILDREN);
|
||||
|
||||
if (ret < 0) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
// make sure that no entries are left in the dict
|
||||
e = NULL;
|
||||
while (e = av_dict_iterate(dict, e)) {
|
||||
av_log(mux, AV_LOG_FATAL, "Unknown submix key %s.\n", e->key);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto fail;
|
||||
}
|
||||
subtoken = av_strtok(NULL, "|", &subptr);
|
||||
}
|
||||
av_freep(&submix_str);
|
||||
|
||||
if (!submix->nb_elements) {
|
||||
av_log(mux, AV_LOG_ERROR, "No audio elements in submix specification \"%s\"\n", token);
|
||||
ret = AVERROR(EINVAL);
|
||||
}
|
||||
token = av_strtok(NULL, ",", &ptr);
|
||||
}
|
||||
|
||||
fail:
|
||||
av_dict_free(&dict);
|
||||
av_free(submix_str);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int of_parse_group_token(Muxer *mux, const char *token, char *ptr)
|
||||
{
|
||||
AVFormatContext *oc = mux->fc;
|
||||
AVStreamGroup *stg;
|
||||
AVDictionary *dict = NULL, *tmp = NULL;
|
||||
const AVDictionaryEntry *e;
|
||||
const AVOption opts[] = {
|
||||
{ "type", "Set group type", offsetof(AVStreamGroup, type), AV_OPT_TYPE_INT,
|
||||
{ .i64 = 0 }, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM, "type" },
|
||||
{ "iamf_audio_element", NULL, 0, AV_OPT_TYPE_CONST,
|
||||
{ .i64 = AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT }, .unit = "type" },
|
||||
{ "iamf_mix_presentation", NULL, 0, AV_OPT_TYPE_CONST,
|
||||
{ .i64 = AV_STREAM_GROUP_PARAMS_IAMF_MIX_PRESENTATION }, .unit = "type" },
|
||||
{ NULL },
|
||||
};
|
||||
const AVClass class = {
|
||||
.class_name = "StreamGroupType",
|
||||
.item_name = av_default_item_name,
|
||||
.option = opts,
|
||||
.version = LIBAVUTIL_VERSION_INT,
|
||||
};
|
||||
const AVClass *pclass = &class;
|
||||
int type, ret;
|
||||
|
||||
ret = av_dict_parse_string(&dict, token, "=", ":", AV_DICT_MULTIKEY);
|
||||
if (ret < 0) {
|
||||
av_log(mux, AV_LOG_ERROR, "Error parsing group specification %s\n", token);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// "type" is not a user settable AVOption in AVStreamGroup, so handle it here
|
||||
e = av_dict_get(dict, "type", NULL, 0);
|
||||
if (!e) {
|
||||
av_log(mux, AV_LOG_ERROR, "No type specified for Stream Group in \"%s\"\n", token);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = av_opt_eval_int(&pclass, opts, e->value, &type);
|
||||
if (!ret && type == AV_STREAM_GROUP_PARAMS_NONE)
|
||||
ret = AVERROR(EINVAL);
|
||||
if (ret < 0) {
|
||||
av_log(mux, AV_LOG_ERROR, "Invalid group type \"%s\"\n", e->value);
|
||||
goto end;
|
||||
}
|
||||
|
||||
av_dict_copy(&tmp, dict, 0);
|
||||
stg = avformat_stream_group_create(oc, type, &tmp);
|
||||
if (!stg) {
|
||||
ret = AVERROR(ENOMEM);
|
||||
goto end;
|
||||
}
|
||||
|
||||
e = NULL;
|
||||
while (e = av_dict_get(dict, "st", e, 0)) {
|
||||
int64_t idx = strtol(e->value, NULL, 0);
|
||||
if (idx < 0 || idx >= oc->nb_streams) {
|
||||
av_log(mux, AV_LOG_ERROR, "Invalid stream index %"PRId64"\n", idx);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto end;
|
||||
}
|
||||
ret = avformat_stream_group_add_stream(stg, oc->streams[idx]);
|
||||
if (ret < 0)
|
||||
goto end;
|
||||
}
|
||||
while (e = av_dict_get(dict, "stg", e, 0)) {
|
||||
int64_t idx = strtol(e->value, NULL, 0);
|
||||
if (idx < 0 || idx >= oc->nb_stream_groups - 1) {
|
||||
av_log(mux, AV_LOG_ERROR, "Invalid stream group index %"PRId64"\n", idx);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto end;
|
||||
}
|
||||
for (unsigned i = 0; i < oc->stream_groups[idx]->nb_streams; i++) {
|
||||
ret = avformat_stream_group_add_stream(stg, oc->stream_groups[idx]->streams[i]);
|
||||
if (ret < 0)
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT:
|
||||
ret = of_parse_iamf_audio_element_layers(mux, stg, ptr);
|
||||
break;
|
||||
case AV_STREAM_GROUP_PARAMS_IAMF_MIX_PRESENTATION:
|
||||
ret = of_parse_iamf_submixes(mux, stg, ptr);
|
||||
break;
|
||||
default:
|
||||
av_log(mux, AV_LOG_FATAL, "Unknown group type %d.\n", type);
|
||||
ret = AVERROR(EINVAL);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret < 0)
|
||||
goto end;
|
||||
|
||||
// make sure that nothing but "st" and "stg" entries are left in the dict
|
||||
e = NULL;
|
||||
av_dict_set(&tmp, "type", NULL, 0);
|
||||
while (e = av_dict_iterate(tmp, e)) {
|
||||
if (!strcmp(e->key, "st") || !strcmp(e->key, "stg"))
|
||||
continue;
|
||||
|
||||
av_log(mux, AV_LOG_FATAL, "Unknown group key %s.\n", e->key);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
end:
|
||||
av_dict_free(&dict);
|
||||
av_dict_free(&tmp);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int of_add_groups(Muxer *mux, const OptionsContext *o)
|
||||
{
|
||||
/* process manually set groups */
|
||||
for (int i = 0; i < o->nb_stream_groups; i++) {
|
||||
const char *token;
|
||||
char *str, *ptr = NULL;
|
||||
int ret = 0;
|
||||
|
||||
str = av_strdup(o->stream_groups[i].u.str);
|
||||
if (!str)
|
||||
return ret;
|
||||
|
||||
token = av_strtok(str, ",", &ptr);
|
||||
if (token)
|
||||
ret = of_parse_group_token(mux, token, ptr);
|
||||
|
||||
av_free(str);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int of_add_programs(Muxer *mux, const OptionsContext *o)
|
||||
{
|
||||
AVFormatContext *oc = mux->fc;
|
||||
@@ -2799,6 +3137,10 @@ int of_open(const OptionsContext *o, const char *filename, Scheduler *sch)
|
||||
if (err < 0)
|
||||
return err;
|
||||
|
||||
err = of_add_groups(mux, o);
|
||||
if (err < 0)
|
||||
return err;
|
||||
|
||||
err = of_add_programs(mux, o);
|
||||
if (err < 0)
|
||||
return err;
|
||||
|
||||
Reference in New Issue
Block a user