The branch, master has been updated
via a979c9b9358aef55612eec4b14c098efba350dcd (commit)
via 5ff8395e7806ad27743829b047067098c288782a (commit)
via 433d18a1d99dbfca48ca1b16e38a2a032d140a7a (commit)
via dec3cc01385c2eda49fec359dd6bf88e5177dec0 (commit)
via e7cf188bb61157870e87eafaa2932ab516f60787 (commit)
via 9dcd25b7cd885c1c576bec1021736e8e5e4c78d6 (commit)
via cce85642c90097e235086e65ddfc0f41c5f90608 (commit)
via c751ad2c3690e2dd16de7dfefa651779e6a9498b (commit)
via 99ec0752d7e47d78b38c4e8767d2615cfc503504 (commit)
via ba0dc3d49eda6b692d7f4ecd4e8c565739685112 (commit)
via 7b18beb477f1716b8abac15df91410dc967be2e9 (commit)
via 7ac1b410e12ff0952be49ad9aff0dba0f0406f8f (commit)
via 6879c8ee5dd0cd8e8643d9ebbc3f95ea34a7fa1f (commit)
from 0242cb36a576721ee6fb9bbbf70616dacb9957b3 (commit)
- Log -----------------------------------------------------------------
commit a979c9b9358aef55612eec4b14c098efba350dcd
Author: James Almer <[email protected]>
AuthorDate: Wed Oct 22 14:55:01 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
tests/ffmpeg: add test for HEIF automatic tile merging
Signed-off-by: James Almer <[email protected]>
diff --git a/tests/fate/ffmpeg.mak b/tests/fate/ffmpeg.mak
index 360e62ebbe..57028a7936 100644
--- a/tests/fate/ffmpeg.mak
+++ b/tests/fate/ffmpeg.mak
@@ -267,3 +267,16 @@ FATE_FFMPEG-$(call ENCDEC2, MPEG2VIDEO, FFV1, NUT,
HSTACK_FILTER PIPE_PROTOCOL F
# test matching by stream disposition
fate-ffmpeg-spec-disposition: CMD = framecrc -i
$(TARGET_SAMPLES)/mpegts/pmtchange.ts -map
'0:disp:visual_impaired+descriptions:1' -c copy
FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MPEGTS,,) += fate-ffmpeg-spec-disposition
+
+# test heif image merging using internally defined filtegraphs
+# picking the stream group if not mapping any specific stream
+fate-ffmpeg-heif-merge: CMD = framecrc -i
$(TARGET_SAMPLES)/heif-conformance/C007.heic
+FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, HEVC, HEVC_PARSER) +=
fate-ffmpeg-heif-merge
+
+# mapping the stream group
+fate-ffmpeg-heif-merge-mapped: CMD = framecrc -i
$(TARGET_SAMPLES)/heif-conformance/C007.heic -map '[0:g:0]'
+FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, HEVC, HEVC_PARSER) +=
fate-ffmpeg-heif-merge-mapped
+
+# binding the internal filtegraph with a caller defined filtergraph
+fate-ffmpeg-heif-merge-filtergraph: CMD = framecrc -i
$(TARGET_SAMPLES)/heif-conformance/C007.heic -filter_complex
"sws_flags=+accurate_rnd+bitexact\;[0:g:0]scale=w=1280:h=720[out]" -map "[out]"
+FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, HEVC, HEVC_PARSER SCALE_FILTER) +=
fate-ffmpeg-heif-merge-filtergraph
diff --git a/tests/ref/fate/ffmpeg-heif-merge b/tests/ref/fate/ffmpeg-heif-merge
new file mode 100644
index 0000000000..4ccc3274f5
--- /dev/null
+++ b/tests/ref/fate/ffmpeg-heif-merge
@@ -0,0 +1,6 @@
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 2560x1440
+#sar 0: 0/1
+0, 0, 0, 1, 5529600, 0x3bf5d001
diff --git a/tests/ref/fate/ffmpeg-heif-merge-filtergraph
b/tests/ref/fate/ffmpeg-heif-merge-filtergraph
new file mode 100644
index 0000000000..dfc8c7d868
--- /dev/null
+++ b/tests/ref/fate/ffmpeg-heif-merge-filtergraph
@@ -0,0 +1,6 @@
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 1280x720
+#sar 0: 0/1
+0, 0, 0, 1, 1382400, 0x0f97ebd5
diff --git a/tests/ref/fate/ffmpeg-heif-merge-mapped
b/tests/ref/fate/ffmpeg-heif-merge-mapped
new file mode 100644
index 0000000000..4ccc3274f5
--- /dev/null
+++ b/tests/ref/fate/ffmpeg-heif-merge-mapped
@@ -0,0 +1,6 @@
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 2560x1440
+#sar 0: 0/1
+0, 0, 0, 1, 5529600, 0x3bf5d001
commit 5ff8395e7806ad27743829b047067098c288782a
Author: James Almer <[email protected]>
AuthorDate: Mon Oct 20 19:04:29 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_demux: create a filtegraph to merge HEIF tiles automatically
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c
index 6c9834746f..6901f59dd0 100644
--- a/fftools/ffmpeg_demux.c
+++ b/fftools/ffmpeg_demux.c
@@ -1641,14 +1641,102 @@ static DemuxStreamGroup
*demux_stream_group_alloc(Demuxer *d, AVStreamGroup *stg
return dsg;
}
+static int istg_parse_tile_grid(const OptionsContext *o, Demuxer *d,
InputStreamGroup *istg)
+{
+ InputFile *f = &d->f;
+ AVFormatContext *ic = d->f.ctx;
+ AVStreamGroup *stg = istg->stg;
+ const AVStreamGroupTileGrid *tg = stg->params.tile_grid;
+ OutputFilterOptions opts;
+ AVBPrint bp;
+ char *graph_str;
+ int autorotate = 1;
+ const char *apply_cropping = NULL;
+ int ret;
+
+ if (tg->nb_tiles == 1)
+ return 0;
+
+ memset(&opts, 0, sizeof(opts));
+
+ opt_match_per_stream_group_int(istg, &o->autorotate, ic, stg, &autorotate);
+ if (autorotate)
+ opts.flags |= OFILTER_FLAG_AUTOROTATE;
+
+ opts.flags |= OFILTER_FLAG_CROP;
+ opt_match_per_stream_group_str(istg, &o->apply_cropping, ic, stg,
&apply_cropping);
+ if (apply_cropping) {
+ char *p;
+ int crop = strtol(apply_cropping, &p, 0);
+ if (*p)
+ return AVERROR(EINVAL);
+ if (!crop)
+ opts.flags &= ~OFILTER_FLAG_CROP;
+ }
+
+ av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
+ for (int i = 0; i < tg->nb_tiles; i++)
+ av_bprintf(&bp, "[%d:g:%d:%d]", f->index, stg->index,
tg->offsets[i].idx);
+ av_bprintf(&bp, "xstack=inputs=%d:layout=", tg->nb_tiles);
+ for (int i = 0; i < tg->nb_tiles - 1; i++)
+ av_bprintf(&bp, "%d_%d|", tg->offsets[i].horizontal,
+ tg->offsets[i].vertical);
+ av_bprintf(&bp, "%d_%d:fill=0x%02X%02X%02X@0x%02X",
tg->offsets[tg->nb_tiles - 1].horizontal,
+
tg->offsets[tg->nb_tiles - 1].vertical,
+ tg->background[0],
tg->background[1],
+ tg->background[2],
tg->background[3]);
+ av_bprintf(&bp, "[%d:g:%d]", f->index, stg->index);
+ ret = av_bprint_finalize(&bp, &graph_str);
+ if (ret < 0)
+ return ret;
+
+ if (tg->coded_width != tg->width || tg->coded_height != tg->height) {
+ opts.crop_top = tg->vertical_offset;
+ opts.crop_bottom = tg->coded_height - tg->height - tg->vertical_offset;
+ opts.crop_left = tg->horizontal_offset;
+ opts.crop_right = tg->coded_width - tg->width - tg->horizontal_offset;
+ }
+
+ for (int i = 0; i < tg->nb_coded_side_data; i++) {
+ const AVPacketSideData *sd = &tg->coded_side_data[i];
+
+ ret = av_packet_side_data_to_frame(&opts.side_data,
&opts.nb_side_data, sd, 0);
+ if (ret < 0 && ret != AVERROR(EINVAL))
+ return ret;
+ }
+
+ ret = fg_create(NULL, graph_str, d->sch, &opts);
+ if (ret < 0)
+ return ret;
+
+ istg->fg = filtergraphs[nb_filtergraphs-1];
+ istg->fg->is_internal = 1;
+
+ return 0;
+}
+
static int istg_add(const OptionsContext *o, Demuxer *d, AVStreamGroup *stg)
{
DemuxStreamGroup *dsg;
+ InputStreamGroup *istg;
+ int ret;
dsg = demux_stream_group_alloc(d, stg);
if (!dsg)
return AVERROR(ENOMEM);
+ istg = &dsg->istg;
+
+ switch (stg->type) {
+ case AV_STREAM_GROUP_PARAMS_TILE_GRID:
+ ret = istg_parse_tile_grid(o, d, istg);
+ if (ret < 0)
+ return ret;
+ break;
+ default:
+ break;
+ }
+
return 0;
}
diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c
index 7c7302bac1..be1dbad479 100644
--- a/fftools/ffmpeg_mux_init.c
+++ b/fftools/ffmpeg_mux_init.c
@@ -1630,6 +1630,12 @@ static int map_auto_video(Muxer *mux, const
OptionsContext *o)
}
switch (istg->stg->type) {
+ case AV_STREAM_GROUP_PARAMS_TILE_GRID: {
+ const AVStreamGroupTileGrid *tg = istg->stg->params.tile_grid;
+ score += tg->width * (int64_t)tg->height
+ + 5000000*!!(istg->stg->disposition &
AV_DISPOSITION_DEFAULT);
+ break;
+ }
default:
continue;
}
commit 433d18a1d99dbfca48ca1b16e38a2a032d140a7a
Author: James Almer <[email protected]>
AuthorDate: Mon Oct 20 19:04:22 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_filter: handle metadata from stream group in relevant the
filtergraphs
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 56dea32ac8..c866980251 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -300,6 +300,8 @@ enum OFilterFlags {
// produce 24-bit audio
OFILTER_FLAG_AUDIO_24BIT = (1 << 1),
OFILTER_FLAG_AUTOSCALE = (1 << 2),
+ OFILTER_FLAG_AUTOROTATE = (1 << 3),
+ OFILTER_FLAG_CROP = (1 << 4),
};
typedef struct OutputFilterOptions {
@@ -333,6 +335,11 @@ typedef struct OutputFilterOptions {
enum AVColorRange color_range;
enum AVAlphaMode alpha_mode;
+ unsigned crop_top;
+ unsigned crop_bottom;
+ unsigned crop_left;
+ unsigned crop_right;
+
enum VideoSyncMethod vsync_method;
AVRational frame_rate;
AVRational max_frame_rate;
@@ -348,6 +355,9 @@ typedef struct OutputFilterOptions {
const enum AVColorRange *color_ranges;
const enum AVAlphaMode *alpha_modes;
+ AVFrameSideData **side_data;
+ int nb_side_data;
+
// for simple filtergraphs only, view specifier passed
// along to the decoder
const ViewSpecifier *vs;
@@ -827,7 +837,8 @@ int ofilter_bind_enc(OutputFilter *ofilter,
* @param graph_desc Graph description; an av_malloc()ed string, filtergraph
* takes ownership of it.
*/
-int fg_create(FilterGraph **pfg, char *graph_desc, Scheduler *sch);
+int fg_create(FilterGraph **pfg, char *graph_desc, Scheduler *sch,
+ const OutputFilterOptions *opts);
void fg_free(FilterGraph **pfg);
diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index c8aec3d3f7..b0244fa774 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -204,6 +204,11 @@ typedef struct OutputFilterPriv {
enum AVColorRange color_range;
enum AVAlphaMode alpha_mode;
+ unsigned crop_top;
+ unsigned crop_bottom;
+ unsigned crop_left;
+ unsigned crop_right;
+
AVFrameSideData **side_data;
int nb_side_data;
@@ -228,6 +233,8 @@ typedef struct OutputFilterPriv {
const enum AVColorRange *color_ranges;
const enum AVAlphaMode *alpha_modes;
+ int32_t displaymatrix[9];
+
AVRational enc_timebase;
int64_t trim_start_us;
int64_t trim_duration_us;
@@ -818,7 +825,7 @@ int ofilter_bind_enc(OutputFilter *ofilter, unsigned
sched_idx_enc,
ofp->needed = ofilter->bound = 1;
av_freep(&ofilter->linklabel);
- ofp->flags = opts->flags;
+ ofp->flags |= opts->flags;
ofp->ts_offset = opts->ts_offset;
ofp->enc_timebase = opts->output_tb;
@@ -1078,7 +1085,8 @@ static const AVClass fg_class = {
.category = AV_CLASS_CATEGORY_FILTER,
};
-int fg_create(FilterGraph **pfg, char *graph_desc, Scheduler *sch)
+int fg_create(FilterGraph **pfg, char *graph_desc, Scheduler *sch,
+ const OutputFilterOptions *opts)
{
FilterGraphPriv *fgp;
FilterGraph *fg;
@@ -1175,11 +1183,13 @@ int fg_create(FilterGraph **pfg, char *graph_desc,
Scheduler *sch)
const enum AVMediaType type =
avfilter_pad_get_type(cur->filter_ctx->output_pads,
cur->pad_idx);
OutputFilter *const ofilter = ofilter_alloc(fg, type);
+ OutputFilterPriv *ofp;
if (!ofilter) {
ret = AVERROR(ENOMEM);
goto fail;
}
+ ofp = ofp_from_ofilter(ofilter);
ofilter->linklabel = cur->name;
cur->name = NULL;
@@ -1189,6 +1199,25 @@ int fg_create(FilterGraph **pfg, char *graph_desc,
Scheduler *sch)
ret = AVERROR(ENOMEM);
goto fail;
}
+
+ // opts should only be needed in this function to fill fields from
filtergraphs
+ // whose output is meant to be treated as if it was stream, e.g.
merged HEIF
+ // tile groups.
+ if (opts) {
+ ofp->flags = opts->flags;
+ ofp->side_data = opts->side_data;
+ ofp->nb_side_data = opts->nb_side_data;
+
+ ofp->crop_top = opts->crop_top;
+ ofp->crop_bottom = opts->crop_bottom;
+ ofp->crop_left = opts->crop_left;
+ ofp->crop_right = opts->crop_right;
+
+ const AVFrameSideData *sd = av_frame_side_data_get(ofp->side_data,
ofp->nb_side_data,
+
AV_FRAME_DATA_DISPLAYMATRIX);
+ if (sd)
+ memcpy(ofp->displaymatrix, sd->data,
sizeof(ofp->displaymatrix));
+ }
}
if (!fg->nb_outputs) {
@@ -1225,7 +1254,7 @@ int fg_create_simple(FilterGraph **pfg,
FilterGraphPriv *fgp;
int ret;
- ret = fg_create(pfg, graph_desc, sch);
+ ret = fg_create(pfg, graph_desc, sch, NULL);
if (ret < 0)
return ret;
fg = *pfg;
@@ -1603,6 +1632,53 @@ static int configure_output_video_filter(FilterGraphPriv
*fgp, AVFilterGraph *gr
if (ret < 0)
return ret;
+ if (ofp->flags & OFILTER_FLAG_CROP) {
+ char crop_buf[64];
+ snprintf(crop_buf, sizeof(crop_buf), "w=iw-%u-%u:h=ih-%u-%u:x=%u:y=%u",
+ ofp->crop_left, ofp->crop_right,
+ ofp->crop_top, ofp->crop_bottom,
+ ofp->crop_left, ofp->crop_top);
+ ret = insert_filter(&last_filter, &pad_idx, "crop", crop_buf);
+ if (ret < 0)
+ return ret;
+ }
+
+ if (ofp->flags & OFILTER_FLAG_AUTOROTATE) {
+ int32_t *displaymatrix = ofp->displaymatrix;
+ double theta;
+
+ theta = get_rotation(displaymatrix);
+
+ if (fabs(theta - 90) < 1.0) {
+ ret = insert_filter(&last_filter, &pad_idx, "transpose",
+ displaymatrix[3] > 0 ? "cclock_flip" :
"clock");
+ } else if (fabs(theta - 180) < 1.0) {
+ if (displaymatrix[0] < 0) {
+ ret = insert_filter(&last_filter, &pad_idx, "hflip", NULL);
+ if (ret < 0)
+ return ret;
+ }
+ if (displaymatrix[4] < 0) {
+ ret = insert_filter(&last_filter, &pad_idx, "vflip", NULL);
+ }
+ } else if (fabs(theta - 270) < 1.0) {
+ ret = insert_filter(&last_filter, &pad_idx, "transpose",
+ displaymatrix[3] < 0 ? "clock_flip" :
"cclock");
+ } else if (fabs(theta) > 1.0) {
+ char rotate_buf[64];
+ snprintf(rotate_buf, sizeof(rotate_buf), "%f*PI/180", theta);
+ ret = insert_filter(&last_filter, &pad_idx, "rotate", rotate_buf);
+ } else if (fabs(theta) < 1.0) {
+ if (displaymatrix && displaymatrix[4] < 0) {
+ ret = insert_filter(&last_filter, &pad_idx, "vflip", NULL);
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ av_frame_side_data_remove(&ofp->side_data, &ofp->nb_side_data,
AV_FRAME_DATA_DISPLAYMATRIX);
+ }
+
if ((ofp->width || ofp->height) && (ofp->flags & OFILTER_FLAG_AUTOSCALE)) {
char args[255];
AVFilterContext *filter;
@@ -2100,12 +2176,11 @@ static int configure_filtergraph(FilterGraph *fg,
FilterGraphThread *fgt)
ret = av_buffersink_get_ch_layout(sink, &ofp->ch_layout);
if (ret < 0)
goto fail;
- av_frame_side_data_free(&ofp->side_data, &ofp->nb_side_data);
sd = av_buffersink_get_side_data(sink, &nb_sd);
if (nb_sd)
for (int j = 0; j < nb_sd; j++) {
ret = av_frame_side_data_clone(&ofp->side_data,
&ofp->nb_side_data,
- sd[j], 0);
+ sd[j],
AV_FRAME_SIDE_DATA_FLAG_REPLACE);
if (ret < 0) {
av_frame_side_data_free(&ofp->side_data,
&ofp->nb_side_data);
goto fail;
diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
index 381360c16a..f51523d2e0 100644
--- a/fftools/ffmpeg_opt.c
+++ b/fftools/ffmpeg_opt.c
@@ -1496,7 +1496,7 @@ int ffmpeg_parse_options(int argc, char **argv, Scheduler
*sch)
/* create complex filtergraphs */
for (int i = 0; i < go.nb_filtergraphs; i++) {
- ret = fg_create(NULL, go.filtergraphs[i], sch);
+ ret = fg_create(NULL, go.filtergraphs[i], sch, NULL);
go.filtergraphs[i] = NULL;
if (ret < 0)
goto fail;
commit dec3cc01385c2eda49fec359dd6bf88e5177dec0
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 21:37:33 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_filter: allow binding unlabeled filtergraphs
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index 642c69e975..c8aec3d3f7 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -1383,6 +1383,33 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter, int comm
"Binding input with label '%s' to input stream %d:%d\n",
ifilter->linklabel, ist->file->index, ist->index);
} else {
+ // try finding an unbound filtergraph output
+ for (int i = 0; i < nb_filtergraphs; i++) {
+ FilterGraph *fg_src = filtergraphs[i];
+
+ if (fg == fg_src)
+ continue;
+
+ for (int j = 0; j < fg_src->nb_outputs; j++) {
+ OutputFilter *ofilter = fg_src->outputs[j];
+
+ if (!ofilter->bound) {
+ if (commit) {
+ av_log(fg, AV_LOG_VERBOSE,
+ "Binding unlabeled filtergraph input to
filtergraph output %d:%d\n", i, j);
+
+ ret = ifilter_bind_fg(ifp, fg_src, j);
+ if (ret < 0) {
+ av_log(fg, AV_LOG_ERROR, "Error binding
filtergraph input %d:%d\n", i, j);
+ return ret;
+ }
+ } else
+ ofp_from_ofilter(ofilter)->needed = 1;
+ return 0;
+ }
+ }
+ }
+
ist = ist_find_unused(type);
if (!ist) {
av_log(fg, AV_LOG_FATAL,
commit e7cf188bb61157870e87eafaa2932ab516f60787
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 21:40:24 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_filter: reindent after the previous commit
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index c4df1f3055..642c69e975 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -1318,16 +1318,16 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter, int comm
if (!ofilter->bound && ofilter->linklabel &&
!strcmp(ofilter->linklabel, ifilter->linklabel)) {
if (commit) {
- av_log(fg, AV_LOG_VERBOSE,
- "Binding input with label '%s' to filtergraph
output %d:%d\n",
- ifilter->linklabel, i, j);
-
- ret = ifilter_bind_fg(ifp, fg_src, j);
- if (ret < 0) {
- av_log(fg, AV_LOG_ERROR, "Error binding filtergraph
input %s\n",
- ifilter->linklabel);
- return ret;
- }
+ av_log(fg, AV_LOG_VERBOSE,
+ "Binding input with label '%s' to filtergraph
output %d:%d\n",
+ ifilter->linklabel, i, j);
+
+ ret = ifilter_bind_fg(ifp, fg_src, j);
+ if (ret < 0) {
+ av_log(fg, AV_LOG_ERROR, "Error binding
filtergraph input %s\n",
+ ifilter->linklabel);
+ return ret;
+ }
} else
ofp_from_ofilter(ofilter)->needed = 1;
return 0;
@@ -1379,9 +1379,9 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter, int comm
ist = input_files[file_idx]->streams[st->index];
if (commit)
- av_log(fg, AV_LOG_VERBOSE,
- "Binding input with label '%s' to input stream %d:%d\n",
- ifilter->linklabel, ist->file->index, ist->index);
+ av_log(fg, AV_LOG_VERBOSE,
+ "Binding input with label '%s' to input stream %d:%d\n",
+ ifilter->linklabel, ist->file->index, ist->index);
} else {
ist = ist_find_unused(type);
if (!ist) {
@@ -1393,20 +1393,20 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter, int comm
}
if (commit)
- av_log(fg, AV_LOG_VERBOSE,
- "Binding unlabeled input %d to input stream %d:%d\n",
- ifilter->index, ist->file->index, ist->index);
+ av_log(fg, AV_LOG_VERBOSE,
+ "Binding unlabeled input %d to input stream %d:%d\n",
+ ifilter->index, ist->file->index, ist->index);
}
av_assert0(ist);
if (commit) {
- ret = ifilter_bind_ist(ifilter, ist, &vs);
- if (ret < 0) {
- av_log(fg, AV_LOG_ERROR,
- "Error binding an input stream to complex filtergraph input
%s.\n",
- ifilter->name);
- return ret;
- }
+ ret = ifilter_bind_ist(ifilter, ist, &vs);
+ if (ret < 0) {
+ av_log(fg, AV_LOG_ERROR,
+ "Error binding an input stream to complex filtergraph input
%s.\n",
+ ifilter->name);
+ return ret;
+ }
}
return 0;
commit 9dcd25b7cd885c1c576bec1021736e8e5e4c78d6
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 21:06:25 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_filter: allow removing filtergraphs that contain unbound
outputs
Actual practical implementation of the previous commit.
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index bf2c6c8c71..56dea32ac8 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -403,6 +403,11 @@ typedef struct FilterGraph {
OutputFilter **outputs;
int nb_outputs;
+ // true when the filtergraph is created internally for
+ // purposes like stream group merging. Meant to be freed
+ // if unbound.
+ int is_internal;
+
const char *graph_desc;
struct AVBPrint graph_print_buf;
} FilterGraph;
diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index 1e1c423789..c4df1f3055 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -193,6 +193,8 @@ typedef struct OutputFilterPriv {
void *log_parent;
char log_name[32];
+ int needed;
+
/* desired output stream properties */
int format;
int width, height;
@@ -813,7 +815,7 @@ int ofilter_bind_enc(OutputFilter *ofilter, unsigned
sched_idx_enc,
av_assert0(!opts->enc ||
ofilter->type == opts->enc->type);
- ofilter->bound = 1;
+ ofp->needed = ofilter->bound = 1;
av_freep(&ofilter->linklabel);
ofp->flags = opts->flags;
@@ -924,7 +926,7 @@ static int ofilter_bind_ifilter(OutputFilter *ofilter,
InputFilterPriv *ifp,
av_assert0(!ofilter->bound);
av_assert0(ofilter->type == ifp->ifilter.type);
- ofilter->bound = 1;
+ ofp->needed = ofilter->bound = 1;
av_freep(&ofilter->linklabel);
ofilter->output_name = av_strdup(opts->name);
@@ -1264,7 +1266,7 @@ int fg_create_simple(FilterGraph **pfg,
return 0;
}
-static int fg_complex_bind_input(FilterGraph *fg, InputFilter *ifilter)
+static int fg_complex_bind_input(FilterGraph *fg, InputFilter *ifilter, int
commit)
{
InputFilterPriv *ifp = ifp_from_ifilter(ifilter);
InputStream *ist = NULL;
@@ -1315,15 +1317,20 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter)
if (!ofilter->bound && ofilter->linklabel &&
!strcmp(ofilter->linklabel, ifilter->linklabel)) {
+ if (commit) {
av_log(fg, AV_LOG_VERBOSE,
"Binding input with label '%s' to filtergraph
output %d:%d\n",
ifilter->linklabel, i, j);
ret = ifilter_bind_fg(ifp, fg_src, j);
- if (ret < 0)
+ if (ret < 0) {
av_log(fg, AV_LOG_ERROR, "Error binding filtergraph
input %s\n",
ifilter->linklabel);
return ret;
+ }
+ } else
+ ofp_from_ofilter(ofilter)->needed = 1;
+ return 0;
}
}
}
@@ -1371,6 +1378,7 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter)
}
ist = input_files[file_idx]->streams[st->index];
+ if (commit)
av_log(fg, AV_LOG_VERBOSE,
"Binding input with label '%s' to input stream %d:%d\n",
ifilter->linklabel, ist->file->index, ist->index);
@@ -1384,12 +1392,14 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter)
return AVERROR(EINVAL);
}
+ if (commit)
av_log(fg, AV_LOG_VERBOSE,
"Binding unlabeled input %d to input stream %d:%d\n",
ifilter->index, ist->file->index, ist->index);
}
av_assert0(ist);
+ if (commit) {
ret = ifilter_bind_ist(ifilter, ist, &vs);
if (ret < 0) {
av_log(fg, AV_LOG_ERROR,
@@ -1397,11 +1407,12 @@ static int fg_complex_bind_input(FilterGraph *fg,
InputFilter *ifilter)
ifilter->name);
return ret;
}
+ }
return 0;
}
-static int bind_inputs(FilterGraph *fg)
+static int bind_inputs(FilterGraph *fg, int commit)
{
// bind filtergraph inputs to input streams or other filtergraphs
for (int i = 0; i < fg->nb_inputs; i++) {
@@ -1411,7 +1422,7 @@ static int bind_inputs(FilterGraph *fg)
if (ifp->bound)
continue;
- ret = fg_complex_bind_input(fg, &ifp->ifilter);
+ ret = fg_complex_bind_input(fg, &ifp->ifilter, commit);
if (ret < 0)
return ret;
}
@@ -1424,27 +1435,49 @@ int fg_finalise_bindings(void)
int ret;
for (int i = 0; i < nb_filtergraphs; i++) {
- ret = bind_inputs(filtergraphs[i]);
+ ret = bind_inputs(filtergraphs[i], 0);
if (ret < 0)
return ret;
}
// check that all outputs were bound
- for (int i = 0; i < nb_filtergraphs; i++) {
+ for (int i = nb_filtergraphs - 1; i >= 0; i--) {
FilterGraph *fg = filtergraphs[i];
+ FilterGraphPriv *fgp = fgp_from_fg(filtergraphs[i]);
for (int j = 0; j < fg->nb_outputs; j++) {
OutputFilter *output = fg->outputs[j];
- if (!output->bound) {
- av_log(fg, AV_LOG_FATAL,
- "Filter '%s' has output %d (%s) unconnected\n",
+ if (!ofp_from_ofilter(output)->needed) {
+ if (!fg->is_internal) {
+ av_log(fg, AV_LOG_FATAL,
+ "Filter '%s' has output %d (%s) unconnected\n",
+ output->name, j,
+ output->linklabel ? (const char *)output->linklabel
: "unlabeled");
+ return AVERROR(EINVAL);
+ }
+
+ av_log(fg, AV_LOG_DEBUG,
+ "Internal filter '%s' has output %d (%s) unconnected.
Removing graph\n",
output->name, j,
output->linklabel ? (const char *)output->linklabel :
"unlabeled");
- return AVERROR(EINVAL);
+ sch_remove_filtergraph(fgp->sch, fgp->sch_idx);
+ fg_free(&filtergraphs[i]);
+ nb_filtergraphs--;
+ if (nb_filtergraphs > 0)
+ memmove(&filtergraphs[i],
+ &filtergraphs[i + 1],
+ (nb_filtergraphs - i) * sizeof(*filtergraphs));
+ break;
}
}
}
+ for (int i = 0; i < nb_filtergraphs; i++) {
+ ret = bind_inputs(filtergraphs[i], 1);
+ if (ret < 0)
+ return ret;
+ }
+
return 0;
}
commit cce85642c90097e235086e65ddfc0f41c5f90608
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 20:08:49 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_sched: add a function to remove a filtergraph from the
scheduler
For the purpose of merging streams in a stream group, a filtergraph can't be
created once we know it will be used. Therefore, allow filtergraphs to be
removed from the scheduler after being added.
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg_sched.c b/fftools/ffmpeg_sched.c
index d08f4a061d..ed85bb16de 100644
--- a/fftools/ffmpeg_sched.c
+++ b/fftools/ffmpeg_sched.c
@@ -405,6 +405,9 @@ static int task_start(SchTask *task)
{
int ret;
+ if (!task->parent)
+ return 0;
+
av_log(task->func_arg, AV_LOG_VERBOSE, "Starting thread...\n");
av_assert0(!task->thread_running);
@@ -454,6 +457,23 @@ static int64_t trailing_dts(const Scheduler *sch, int
count_finished)
return min_dts == INT64_MAX ? AV_NOPTS_VALUE : min_dts;
}
+void sch_remove_filtergraph(Scheduler *sch, int idx)
+{
+ SchFilterGraph *fg = &sch->filters[idx];
+
+ av_assert0(!fg->task.thread_running);
+ memset(&fg->task, 0, sizeof(fg->task));
+
+ tq_free(&fg->queue);
+
+ av_freep(&fg->inputs);
+ fg->nb_inputs = 0;
+ av_freep(&fg->outputs);
+ fg->nb_outputs = 0;
+
+ fg->task_exited = 1;
+}
+
void sch_free(Scheduler **psch)
{
Scheduler *sch = *psch;
@@ -2630,6 +2650,9 @@ static int task_stop(Scheduler *sch, SchTask *task)
int ret;
void *thread_ret;
+ if (!task->parent)
+ return 0;
+
if (!task->thread_running)
return task_cleanup(sch, task->node);
diff --git a/fftools/ffmpeg_sched.h b/fftools/ffmpeg_sched.h
index 0c01f558e4..2cf3034437 100644
--- a/fftools/ffmpeg_sched.h
+++ b/fftools/ffmpeg_sched.h
@@ -205,6 +205,8 @@ int sch_add_dec_output(Scheduler *sch, unsigned dec_idx);
int sch_add_filtergraph(Scheduler *sch, unsigned nb_inputs, unsigned
nb_outputs,
SchThreadFunc func, void *ctx);
+void sch_remove_filtergraph(Scheduler *sch, int idx);
+
/**
* Add a muxer to the scheduler.
*
commit c751ad2c3690e2dd16de7dfefa651779e6a9498b
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 21:02:21 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_mux_init: allow creating streams from filtergraphs created
out of stream groups
Several formats are being designed where more than one independent video or
audio stream within a container are part of what should be a single combined
output. This is the case for HEIF (Defining a grid where the decoded output
of several separate video streams are to be placed to generate a single
output
image) and IAMF (Defining audio streams where channels are present in
separate
coded stream).
AVStreamGroup was designed and implemented in libavformat to convey this
information, but the actual merging is left to the caller.
This change allows the FFmpeg CLI to take said information, parse it, and
create filtergraphs to merge the streams, making the combined output be
usable
automatically further in the process.
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 0b9f8a20c6..bf2c6c8c71 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -500,6 +500,7 @@ typedef struct InputStreamGroup {
int index;
+ FilterGraph *fg;
AVStreamGroup *stg;
} InputStreamGroup;
diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c
index c24b69f2d1..7c7302bac1 100644
--- a/fftools/ffmpeg_mux_init.c
+++ b/fftools/ffmpeg_mux_init.c
@@ -1598,6 +1598,7 @@ fail:
static int map_auto_video(Muxer *mux, const OptionsContext *o)
{
AVFormatContext *oc = mux->fc;
+ InputStreamGroup *best_istg = NULL;
InputStream *best_ist = NULL;
int64_t best_score = 0;
int qcr;
@@ -1609,8 +1610,35 @@ static int map_auto_video(Muxer *mux, const
OptionsContext *o)
qcr = avformat_query_codec(oc->oformat, oc->oformat->video_codec, 0);
for (int j = 0; j < nb_input_files; j++) {
InputFile *ifile = input_files[j];
+ InputStreamGroup *file_best_istg = NULL;
InputStream *file_best_ist = NULL;
int64_t file_best_score = 0;
+ for (int i = 0; i < ifile->nb_stream_groups; i++) {
+ InputStreamGroup *istg = ifile->stream_groups[i];
+ int64_t score = 0;
+
+ if (!istg->fg)
+ continue;
+
+ for (int j = 0; j < istg->stg->nb_streams; j++) {
+ AVStream *st = istg->stg->streams[j];
+
+ if (st->event_flags & AVSTREAM_EVENT_FLAG_NEW_PACKETS) {
+ score = 100000000;
+ break;
+ }
+ }
+
+ switch (istg->stg->type) {
+ default:
+ continue;
+ }
+
+ if (score > file_best_score) {
+ file_best_score = score;
+ file_best_istg = istg;
+ }
+ }
for (int i = 0; i < ifile->nb_streams; i++) {
InputStream *ist = ifile->streams[i];
int64_t score;
@@ -1630,6 +1658,15 @@ static int map_auto_video(Muxer *mux, const
OptionsContext *o)
continue;
file_best_score = score;
file_best_ist = ist;
+ file_best_istg = NULL;
+ }
+ }
+ if (file_best_istg) {
+ file_best_score -= 5000000*!!(file_best_istg->stg->disposition &
AV_DISPOSITION_DEFAULT);
+ if (file_best_score > best_score) {
+ best_score = file_best_score;
+ best_istg = file_best_istg;
+ best_ist = NULL;
}
}
if (file_best_ist) {
@@ -1639,9 +1676,19 @@ static int map_auto_video(Muxer *mux, const
OptionsContext *o)
if (file_best_score > best_score) {
best_score = file_best_score;
best_ist = file_best_ist;
+ best_istg = NULL;
}
}
}
+ if (best_istg) {
+ FilterGraph *fg = best_istg->fg;
+ OutputFilter *ofilter = fg->outputs[0];
+
+ av_assert0(fg->nb_outputs == 1);
+ av_log(mux, AV_LOG_VERBOSE, "Creating output stream from stream group
derived complex filtergraph %d.\n", fg->index);
+
+ return ost_add(mux, o, AVMEDIA_TYPE_VIDEO, NULL, ofilter, NULL, NULL);
+ }
if (best_ist)
return ost_add(mux, o, AVMEDIA_TYPE_VIDEO, best_ist, NULL, NULL, NULL);
commit 99ec0752d7e47d78b38c4e8767d2615cfc503504
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 20:58:21 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_demux: add InputStreamGroup to store stream groups
Preparatory work for upcoming changes. Make ffmpeg keep track of stream
groups
internally.
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 155674d303..0b9f8a20c6 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -492,6 +492,17 @@ typedef struct InputStream {
int nb_filters;
} InputStream;
+typedef struct InputStreamGroup {
+ const AVClass *class;
+
+ /* parent source */
+ struct InputFile *file;
+
+ int index;
+
+ AVStreamGroup *stg;
+} InputStreamGroup;
+
typedef struct InputFile {
const AVClass *class;
@@ -513,6 +524,10 @@ typedef struct InputFile {
* if new streams appear dynamically during demuxing */
InputStream **streams;
int nb_streams;
+
+ /* stream groups that ffmpeg is aware of; */
+ InputStreamGroup **stream_groups;
+ int nb_stream_groups;
} InputFile;
enum forced_keyframes_const {
diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c
index d2f0017aeb..6c9834746f 100644
--- a/fftools/ffmpeg_demux.c
+++ b/fftools/ffmpeg_demux.c
@@ -104,6 +104,13 @@ typedef struct DemuxStream {
int64_t lag;
} DemuxStream;
+typedef struct DemuxStreamGroup {
+ InputStreamGroup istg;
+
+ // name used for logging
+ char log_name[32];
+} DemuxStreamGroup;
+
typedef struct Demuxer {
InputFile f;
@@ -886,6 +893,16 @@ static void ist_free(InputStream **pist)
av_freep(pist);
}
+static void istg_free(InputStreamGroup **pistg)
+{
+ InputStreamGroup *istg = *pistg;
+
+ if (!istg)
+ return;
+
+ av_freep(pistg);
+}
+
void ifile_close(InputFile **pf)
{
InputFile *f = *pf;
@@ -901,6 +918,10 @@ void ifile_close(InputFile **pf)
ist_free(&f->streams[i]);
av_freep(&f->streams);
+ for (int i = 0; i < f->nb_stream_groups; i++)
+ istg_free(&f->stream_groups[i]);
+ av_freep(&f->stream_groups);
+
avformat_close_input(&f->ctx);
av_packet_free(&d->pkt_heartbeat);
@@ -1586,6 +1607,51 @@ static int ist_add(const OptionsContext *o, Demuxer *d,
AVStream *st, AVDictiona
return 0;
}
+static const char *input_stream_group_item_name(void *obj)
+{
+ const DemuxStreamGroup *dsg = obj;
+
+ return dsg->log_name;
+}
+
+static const AVClass input_stream_group_class = {
+ .class_name = "InputStreamGroup",
+ .version = LIBAVUTIL_VERSION_INT,
+ .item_name = input_stream_group_item_name,
+ .category = AV_CLASS_CATEGORY_DEMUXER,
+};
+
+static DemuxStreamGroup *demux_stream_group_alloc(Demuxer *d, AVStreamGroup
*stg)
+{
+ InputFile *f = &d->f;
+ DemuxStreamGroup *dsg;
+
+ dsg = allocate_array_elem(&f->stream_groups, sizeof(*dsg),
&f->nb_stream_groups);
+ if (!dsg)
+ return NULL;
+
+ dsg->istg.stg = stg;
+ dsg->istg.file = f;
+ dsg->istg.index = stg->index;
+ dsg->istg.class = &input_stream_group_class;
+
+ snprintf(dsg->log_name, sizeof(dsg->log_name), "istg#%d:%d/%s",
+ d->f.index, stg->index, avformat_stream_group_name(stg->type));
+
+ return dsg;
+}
+
+static int istg_add(const OptionsContext *o, Demuxer *d, AVStreamGroup *stg)
+{
+ DemuxStreamGroup *dsg;
+
+ dsg = demux_stream_group_alloc(d, stg);
+ if (!dsg)
+ return AVERROR(ENOMEM);
+
+ return 0;
+}
+
static int dump_attachment(InputStream *ist, const char *filename)
{
AVStream *st = ist->st;
@@ -1954,6 +2020,13 @@ int ifile_open(const OptionsContext *o, const char
*filename, Scheduler *sch)
}
}
+ /* Add all the stream groups from the given input file to the demuxer */
+ for (int i = 0; i < ic->nb_stream_groups; i++) {
+ ret = istg_add(o, d, ic->stream_groups[i]);
+ if (ret < 0)
+ return ret;
+ }
+
/* dump the file content */
av_dump_format(ic, f->index, filename, 0);
commit ba0dc3d49eda6b692d7f4ecd4e8c565739685112
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 20:46:14 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_opt: add helpers to match stream groups
Will be used to check for specifiers that match a given stream group and not
a stream within one.
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/cmdutils.c b/fftools/cmdutils.c
index 5f79d269b2..35f786dba5 100644
--- a/fftools/cmdutils.c
+++ b/fftools/cmdutils.c
@@ -1350,6 +1350,77 @@ int check_stream_specifier(AVFormatContext *s, AVStream
*st, const char *spec)
return ret;
}
+unsigned stream_group_specifier_match(const StreamSpecifier *ss,
+ const AVFormatContext *s, const
AVStreamGroup *stg,
+ void *logctx)
+{
+ int start_stream_group = 0, nb_stream_groups;
+ int nb_matched = 0;
+
+ if (ss->idx >= 0)
+ return 0;
+
+ switch (ss->stream_list) {
+ case STREAM_LIST_STREAM_ID:
+ case STREAM_LIST_ALL:
+ case STREAM_LIST_PROGRAM:
+ return 0;
+ case STREAM_LIST_GROUP_ID:
+ // <n-th> stream with given ID makes no sense and should be impossible
to request
+ av_assert0(ss->idx < 0);
+ // return early if we know for sure the stream does not match
+ if (stg->id != ss->list_id)
+ return 0;
+ start_stream_group = stg->index;
+ nb_stream_groups = stg->index + 1;
+ break;
+ case STREAM_LIST_GROUP_IDX:
+ start_stream_group = ss->list_id >= 0 ? 0 : stg->index;
+ nb_stream_groups = stg->index + 1;
+ break;
+ default: av_assert0(0);
+ }
+
+ for (int i = start_stream_group; i < nb_stream_groups; i++) {
+ const AVStreamGroup *candidate = s->stream_groups[i];
+
+ if (ss->meta_key) {
+ const AVDictionaryEntry *tag = av_dict_get(candidate->metadata,
+ ss->meta_key, NULL, 0);
+
+ if (!tag)
+ continue;
+ if (ss->meta_val && strcmp(tag->value, ss->meta_val))
+ continue;
+ }
+
+ if (ss->usable_only) {
+ switch (candidate->type) {
+ case AV_STREAM_GROUP_PARAMS_TILE_GRID: {
+ const AVStreamGroupTileGrid *tg = candidate->params.tile_grid;
+ if (!tg->coded_width || !tg->coded_height || !tg->nb_tiles ||
+ !tg->width || !tg->height || !tg->nb_tiles)
+ continue;
+ break;
+ }
+ default:
+ continue;
+ }
+ }
+
+ if (ss->disposition &&
+ (candidate->disposition & ss->disposition) != ss->disposition)
+ continue;
+
+ if (stg == candidate)
+ return ss->list_id < 0 || ss->list_id == nb_matched;
+
+ nb_matched++;
+ }
+
+ return 0;
+}
+
int filter_codec_opts(const AVDictionary *opts, enum AVCodecID codec_id,
AVFormatContext *s, AVStream *st, const AVCodec *codec,
AVDictionary **dst, AVDictionary **opts_used)
diff --git a/fftools/cmdutils.h b/fftools/cmdutils.h
index c4a3efcf52..93e05c7130 100644
--- a/fftools/cmdutils.h
+++ b/fftools/cmdutils.h
@@ -158,6 +158,10 @@ unsigned stream_specifier_match(const StreamSpecifier *ss,
const AVFormatContext *s, const AVStream *st,
void *logctx);
+unsigned stream_group_specifier_match(const StreamSpecifier *ss,
+ const AVFormatContext *s, const
AVStreamGroup *stg,
+ void *logctx);
+
void stream_specifier_uninit(StreamSpecifier *ss);
typedef struct SpecifierOpt {
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 8f665f3432..155674d303 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -136,6 +136,7 @@ typedef struct StreamMap {
int disabled; /* 1 is this mapping is disabled by a negative map
*/
int file_index;
int stream_index;
+ int group_index;
char *linklabel; /* name of an output link, for mapping lavfi
outputs */
ViewSpecifier vs;
@@ -932,6 +933,15 @@ void opt_match_per_stream_int64(void *logctx, const
SpecifierOptList *sol,
void opt_match_per_stream_dbl(void *logctx, const SpecifierOptList *sol,
AVFormatContext *fc, AVStream *st, double *out);
+void opt_match_per_stream_group_str(void *logctx, const SpecifierOptList *sol,
+ AVFormatContext *fc, AVStreamGroup *stg,
const char **out);
+void opt_match_per_stream_group_int(void *logctx, const SpecifierOptList *sol,
+ AVFormatContext *fc, AVStreamGroup *stg,
int *out);
+void opt_match_per_stream_group_int64(void *logctx, const SpecifierOptList
*sol,
+ AVFormatContext *fc, AVStreamGroup *stg,
int64_t *out);
+void opt_match_per_stream_group_dbl(void *logctx, const SpecifierOptList *sol,
+ AVFormatContext *fc, AVStreamGroup *stg,
double *out);
+
int view_specifier_parse(const char **pspec, ViewSpecifier *vs);
int muxer_thread(void *arg);
diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
index 4152bbf06f..381360c16a 100644
--- a/fftools/ffmpeg_opt.c
+++ b/fftools/ffmpeg_opt.c
@@ -242,6 +242,70 @@ OPT_MATCH_PER_STREAM(int, int, OPT_TYPE_INT,
i);
OPT_MATCH_PER_STREAM(int64, int64_t, OPT_TYPE_INT64, i64);
OPT_MATCH_PER_STREAM(dbl, double, OPT_TYPE_DOUBLE, dbl);
+static unsigned opt_match_per_stream_group(void *logctx, enum OptionType type,
+ const SpecifierOptList *sol,
+ AVFormatContext *fc, AVStreamGroup
*stg)
+{
+ int matches = 0, match_idx = -1;
+
+ av_assert0((type == sol->type) || !sol->nb_opt);
+
+ for (int i = 0; i < sol->nb_opt; i++) {
+ const StreamSpecifier *ss = &sol->opt[i].stream_spec;
+
+ if (stream_group_specifier_match(ss, fc, stg, logctx)) {
+ match_idx = i;
+ matches++;
+ }
+ }
+
+ if (matches > 1 && sol->opt_canon) {
+ const SpecifierOpt *so = &sol->opt[match_idx];
+ const char *spec = so->specifier && so->specifier[0] ? so->specifier :
"";
+
+ char namestr[128] = "";
+ char optval_buf[32];
+ const char *optval = optval_buf;
+
+ snprintf(namestr, sizeof(namestr), "-%s", sol->opt_canon->name);
+ if (sol->opt_canon->flags & OPT_HAS_ALT) {
+ const char * const *names_alt = sol->opt_canon->u1.names_alt;
+ for (int i = 0; names_alt[i]; i++)
+ av_strlcatf(namestr, sizeof(namestr), "/-%s", names_alt[i]);
+ }
+
+ switch (sol->type) {
+ case OPT_TYPE_STRING: optval = so->u.str;
break;
+ case OPT_TYPE_INT: snprintf(optval_buf, sizeof(optval_buf), "%d",
so->u.i); break;
+ case OPT_TYPE_INT64: snprintf(optval_buf, sizeof(optval_buf),
"%"PRId64, so->u.i64); break;
+ case OPT_TYPE_FLOAT: snprintf(optval_buf, sizeof(optval_buf), "%f",
so->u.f); break;
+ case OPT_TYPE_DOUBLE: snprintf(optval_buf, sizeof(optval_buf), "%f",
so->u.dbl); break;
+ default: av_assert0(0);
+ }
+
+ av_log(logctx, AV_LOG_WARNING, "Multiple %s options specified for "
+ "stream group %d, only the last option '-%s%s%s %s' will be
used.\n",
+ namestr, stg->index, sol->opt_canon->name, spec[0] ? ":" : "",
+ spec, optval);
+ }
+
+ return match_idx + 1;
+}
+
+#define OPT_MATCH_PER_STREAM_GROUP(name, type, opt_type, m)
\
+void opt_match_per_stream_group_ ## name(void *logctx, const SpecifierOptList
*sol, \
+ AVFormatContext *fc, AVStreamGroup
*stg, type *out) \
+{
\
+ unsigned ret = opt_match_per_stream_group(logctx, opt_type, sol, fc, stg);
\
+ if (ret > 0)
\
+ *out = sol->opt[ret - 1].u.m;
\
+}
+
+OPT_MATCH_PER_STREAM_GROUP(str, const char *, OPT_TYPE_STRING, str);
+OPT_MATCH_PER_STREAM_GROUP(int, int, OPT_TYPE_INT, i);
+OPT_MATCH_PER_STREAM_GROUP(int64, int64_t, OPT_TYPE_INT64, i64);
+OPT_MATCH_PER_STREAM_GROUP(dbl, double, OPT_TYPE_DOUBLE, dbl);
+
int view_specifier_parse(const char **pspec, ViewSpecifier *vs)
{
const char *spec = *pspec;
@@ -504,8 +568,10 @@ static int opt_map(void *optctx, const char *opt, const
char *arg)
}
if (arg[0] == '[') {
+ ViewSpecifier vs;
/* this mapping refers to lavfi output */
const char *c = arg + 1;
+ char *endptr;
ret = GROW_ARRAY(o->stream_maps, o->nb_stream_maps);
if (ret < 0)
@@ -518,6 +584,27 @@ static int opt_map(void *optctx, const char *opt, const
char *arg)
ret = AVERROR(EINVAL);
goto fail;
}
+
+ arg++;
+
+ m->group_index = -1;
+ file_idx = strtol(arg, &endptr, 0);
+ if (file_idx >= nb_input_files || file_idx < 0)
+ goto end;
+
+ arg = endptr;
+ ret = stream_specifier_parse(&ss, *arg == ':' ? arg + 1 : arg, 1,
NULL);
+ if (ret < 0)
+ goto end;
+
+ arg = ss.remainder ? ss.remainder : "";
+ ret = view_specifier_parse(&arg, &vs);
+ if (ret < 0 || (*arg && strcmp(arg, "]")))
+ goto end;
+
+ m->file_index = file_idx;
+ m->stream_index = ss.idx;
+ m->group_index = ss.stream_list == STREAM_LIST_GROUP_IDX ? ss.list_id
: -1;
} else {
ViewSpecifier vs;
char *endptr;
@@ -583,6 +670,7 @@ static int opt_map(void *optctx, const char *opt, const
char *arg)
m->file_index = file_idx;
m->stream_index = i;
+ m->group_index = ss.stream_list == STREAM_LIST_GROUP_IDX ?
ss.list_id : -1;
m->vs = vs;
}
}
@@ -602,6 +690,7 @@ static int opt_map(void *optctx, const char *opt, const
char *arg)
goto fail;
}
}
+end:
ret = 0;
fail:
stream_specifier_uninit(&ss);
commit 7b18beb477f1716b8abac15df91410dc967be2e9
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 20:38:39 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools: pass global side data through using a separate array
This keeps global and per frame side data clearly separated, and actually
propagates the former as it comes out from the buffersink filter.
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c
index cbeefb71d6..32289112a8 100644
--- a/fftools/ffmpeg.c
+++ b/fftools/ffmpeg.c
@@ -400,6 +400,7 @@ static void frame_data_free(void *opaque, uint8_t *data)
{
FrameData *fd = (FrameData *)data;
+ av_frame_side_data_free(&fd->side_data, &fd->nb_side_data);
avcodec_parameters_free(&fd->par_enc);
av_free(data);
@@ -429,6 +430,8 @@ static int frame_data_ensure(AVBufferRef **dst, int
writable)
memcpy(fd, fd_src, sizeof(*fd));
fd->par_enc = NULL;
+ fd->side_data = NULL;
+ fd->nb_side_data = 0;
if (fd_src->par_enc) {
int ret = 0;
@@ -444,6 +447,16 @@ static int frame_data_ensure(AVBufferRef **dst, int
writable)
}
}
+ if (fd_src->nb_side_data) {
+ int ret = clone_side_data(&fd->side_data, &fd->nb_side_data,
+ fd_src->side_data,
fd_src->nb_side_data, 0);
+ if (ret < 0) {
+ av_buffer_unref(dst);
+ av_buffer_unref(&src);
+ return ret;
+ }
+ }
+
av_buffer_unref(&src);
} else {
fd->dec.frame_num = UINT64_MAX;
diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index 63da1d14df..8f665f3432 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -698,6 +698,9 @@ typedef struct FrameData {
int64_t wallclock[LATENCY_PROBE_NB];
AVCodecParameters *par_enc;
+
+ AVFrameSideData **side_data;
+ int nb_side_data;
} FrameData;
extern InputFile **input_files;
diff --git a/fftools/ffmpeg_enc.c b/fftools/ffmpeg_enc.c
index 84e7e0ca0e..8f07a10848 100644
--- a/fftools/ffmpeg_enc.c
+++ b/fftools/ffmpeg_enc.c
@@ -205,19 +205,10 @@ int enc_open(void *opaque, const AVFrame *frame)
av_assert0(frame->opaque_ref);
fd = (FrameData*)frame->opaque_ref->data;
- for (int i = 0; i < frame->nb_side_data; i++) {
- const AVSideDataDescriptor *desc =
av_frame_side_data_desc(frame->side_data[i]->type);
-
- if (!(desc->props & AV_SIDE_DATA_PROP_GLOBAL))
- continue;
-
- ret = av_frame_side_data_clone(&enc_ctx->decoded_side_data,
- &enc_ctx->nb_decoded_side_data,
- frame->side_data[i],
- AV_FRAME_SIDE_DATA_FLAG_UNIQUE);
- if (ret < 0)
- return ret;
- }
+ ret = clone_side_data(&enc_ctx->decoded_side_data,
&enc_ctx->nb_decoded_side_data,
+ fd->side_data, fd->nb_side_data,
AV_FRAME_SIDE_DATA_FLAG_UNIQUE);
+ if (ret < 0)
+ return ret;
}
if (ist)
diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index a23bcbda95..1e1c423789 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -2129,7 +2129,8 @@ static int ifilter_parameters_from_frame(InputFilter
*ifilter, const AVFrame *fr
for (int i = 0; i < frame->nb_side_data; i++) {
const AVSideDataDescriptor *desc =
av_frame_side_data_desc(frame->side_data[i]->type);
- if (!(desc->props & AV_SIDE_DATA_PROP_GLOBAL))
+ if (!(desc->props & AV_SIDE_DATA_PROP_GLOBAL) ||
+ frame->side_data[i]->type == AV_FRAME_DATA_DISPLAYMATRIX)
continue;
ret = av_frame_side_data_clone(&ifp->side_data,
@@ -2502,16 +2503,17 @@ static int close_output(OutputFilterPriv *ofp,
FilterGraphThread *fgt)
if (ret < 0)
return ret;
}
- av_frame_side_data_free(&frame->side_data, &frame->nb_side_data);
- ret = clone_side_data(&frame->side_data, &frame->nb_side_data,
- ofp->side_data, ofp->nb_side_data, 0);
- if (ret < 0)
- return ret;
fd = frame_data(frame);
if (!fd)
return AVERROR(ENOMEM);
+ av_frame_side_data_free(&fd->side_data, &fd->nb_side_data);
+ ret = clone_side_data(&fd->side_data, &fd->nb_side_data,
+ ofp->side_data, ofp->nb_side_data, 0);
+ if (ret < 0)
+ return ret;
+
fd->frame_rate_filter = ofp->fps.framerate;
av_assert0(!frame->buf[0]);
@@ -2666,6 +2668,14 @@ static int fg_output_step(OutputFilterPriv *ofp,
FilterGraphThread *fgt,
return AVERROR(ENOMEM);
}
+ av_frame_side_data_free(&fd->side_data, &fd->nb_side_data);
+ if (!fgt->got_frame) {
+ ret = clone_side_data(&fd->side_data, &fd->nb_side_data,
+ ofp->side_data, ofp->nb_side_data, 0);
+ if (ret < 0)
+ return ret;
+ }
+
fd->wallclock[LATENCY_PROBE_FILTER_POST] = av_gettime_relative();
// only use bits_per_raw_sample passed through from the decoder
commit 7ac1b410e12ff0952be49ad9aff0dba0f0406f8f
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 20:28:46 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 11:02:01 2025 -0300
fftools/ffmpeg_filter: fix passing certain parameters to inputs bound to a
filtergraph output
Certain parameters, like calculated framerate, are unavailable when
connecting
the output of a filtergraph to the input of another.
This fixes command lines like
ffmpeg -lavfi "testsrc=rate=1:duration=1[out0]" -filter_complex
"[out0]null[out1]" -map [out1] -y out.png
Signed-off-by: James Almer <[email protected]>
diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index b9a51ad687..a23bcbda95 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -109,6 +109,9 @@ typedef struct InputFilterPriv {
// used to hold submitted input
AVFrame *frame;
+ // For inputs bound to a filtergraph output
+ OutputFilter *ofilter_src;
+
// source data type: AVMEDIA_TYPE_SUBTITLE for sub2video,
// same as type otherwise
enum AVMediaType type_src;
@@ -928,6 +931,8 @@ static int ofilter_bind_ifilter(OutputFilter *ofilter,
InputFilterPriv *ifp,
if (!ofilter->output_name)
return AVERROR(EINVAL);
+ ifp->ofilter_src = ofilter;
+
av_strlcatf(ofp->log_name, sizeof(ofp->log_name), "->%s",
ofilter->output_name);
return 0;
@@ -2155,6 +2160,27 @@ static int ifilter_parameters_from_frame(InputFilter
*ifilter, const AVFrame *fr
return 0;
}
+static int ifilter_parameters_from_ofilter(InputFilter *ifilter, OutputFilter
*ofilter)
+{
+ const OutputFilterPriv *ofp = ofp_from_ofilter(ofilter);
+ InputFilterPriv *ifp = ifp_from_ifilter(ifilter);
+
+ if (!ifp->opts.framerate.num) {
+ ifp->opts.framerate = ofp->fps.framerate;
+ if (ifp->opts.framerate.num > 0 && ifp->opts.framerate.den > 0)
+ ifp->opts.flags |= IFILTER_FLAG_CFR;
+ }
+
+ for (int i = 0; i < ofp->nb_side_data; i++) {
+ int ret = av_frame_side_data_clone(&ifp->side_data, &ifp->nb_side_data,
+ ofp->side_data[i],
AV_FRAME_SIDE_DATA_FLAG_REPLACE);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
int filtergraph_is_simple(const FilterGraph *fg)
{
const FilterGraphPriv *fgp = cfgp_from_cfg(fg);
@@ -2935,6 +2961,14 @@ static int send_frame(FilterGraph *fg, FilterGraphThread
*fgt,
ret = ifilter_parameters_from_frame(ifilter, frame);
if (ret < 0)
return ret;
+
+ /* Inputs bound to a filtergraph output will have some fields unset.
+ * Handle them here */
+ if (ifp->ofilter_src) {
+ ret = ifilter_parameters_from_ofilter(ifilter, ifp->ofilter_src);
+ if (ret < 0)
+ return ret;
+ }
}
/* (re)init the graph if possible, otherwise buffer the frame and return */
commit 6879c8ee5dd0cd8e8643d9ebbc3f95ea34a7fa1f
Author: James Almer <[email protected]>
AuthorDate: Sat Oct 18 20:07:15 2025 -0300
Commit: James Almer <[email protected]>
CommitDate: Thu Oct 30 10:54:01 2025 -0300
avcodec/avcodec: add helpers to convert between packet and frame side data
They will be used in following commits.
Signed-off-by: James Almer <[email protected]>
diff --git a/doc/APIchanges b/doc/APIchanges
index 7c2ab3cc03..7de9072a47 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -2,6 +2,9 @@ The last version increases of all libraries were on 2025-03-28
API changes, most recent first:
+2025-10-30 - xxxxxxxxxx - lavc 62.17.100 - packet.h
+ Add av_packet_side_data_from_frame() and av_packet_side_data_to_frame().
+
2025-10-xx - xxxxxxxxxx - lavu 60.16.100 - pixfmt.h
Add AVCOL_TRC_EXT_BASE, AVCOL_TRC_V_LOG,
AVCOL_PRI_EXT_BASE and AVCOL_PRI_V_GAMUT.
diff --git a/libavcodec/avcodec.c b/libavcodec/avcodec.c
index 0355b7c338..db24b4ed52 100644
--- a/libavcodec/avcodec.c
+++ b/libavcodec/avcodec.c
@@ -815,3 +815,53 @@ int avcodec_get_supported_config(const AVCodecContext
*avctx, const AVCodec *cod
return ff_default_get_supported_config(avctx, codec, config, flags,
out, out_num);
}
}
+
+int av_packet_side_data_from_frame(AVPacketSideData **psd, int *pnb_sd,
+ const AVFrameSideData *src, unsigned int
flags)
+{
+ AVPacketSideData *sd = NULL;
+
+ for (unsigned j = 0; ff_sd_global_map[j].packet < AV_PKT_DATA_NB; j++) {
+ if (ff_sd_global_map[j].frame != src->type)
+ continue;
+
+ sd = av_packet_side_data_new(psd, pnb_sd, ff_sd_global_map[j].packet,
+ src->size, 0);
+
+ if (!sd)
+ return AVERROR(ENOMEM);
+
+ memcpy(sd->data, src->data, src->size);
+ break;
+ }
+
+ if (!sd)
+ return AVERROR(EINVAL);
+
+ return 0;
+}
+
+int av_packet_side_data_to_frame(AVFrameSideData ***psd, int *pnb_sd,
+ const AVPacketSideData *src, unsigned int
flags)
+{
+ AVFrameSideData *sd = NULL;
+
+ for (unsigned j = 0; ff_sd_global_map[j].packet < AV_PKT_DATA_NB; j++) {
+ if (ff_sd_global_map[j].packet != src->type)
+ continue;
+
+ sd = av_frame_side_data_new(psd, pnb_sd, ff_sd_global_map[j].frame,
+ src->size, flags);
+
+ if (!sd)
+ return AVERROR(ENOMEM);
+
+ memcpy(sd->data, src->data, src->size);
+ break;
+ }
+
+ if (!sd)
+ return AVERROR(EINVAL);
+
+ return 0;
+}
diff --git a/libavcodec/packet.h b/libavcodec/packet.h
index 5e27f9ceb5..59bfddf4cc 100644
--- a/libavcodec/packet.h
+++ b/libavcodec/packet.h
@@ -489,6 +489,36 @@ void av_packet_side_data_remove(AVPacketSideData *sd, int
*nb_sd,
*/
void av_packet_side_data_free(AVPacketSideData **sd, int *nb_sd);
+struct AVFrameSideData;
+
+/**
+ * Add a new packet side data entry to an array based on existing frame
+ * side data, if a matching type exists for packet side data.
+ *
+ * @param flags Currently unused. Must be 0.
+ * @retval >= 0 Success
+ * @retval AVERROR(EINVAL) The frame side data type does not have a matching
+ * packet side data type.
+ * @retval AVERROR(ENOMEM) Failed to add a side data entry to the array, or
+ * similar.
+ */
+int av_packet_side_data_from_frame(AVPacketSideData **sd, int *nb_sd,
+ const struct AVFrameSideData *src, unsigned
int flags);
+/**
+ * Add a new frame side data entry to an array based on existing packet
+ * side data, if a matching type exists for frame side data.
+ *
+ * @param flags Some combination of AV_FRAME_SIDE_DATA_FLAG_*
flags,
+ * or 0.
+ * @retval >= 0 Success
+ * @retval AVERROR(EINVAL) The packet side data type does not have a matching
+ * frame side data type.
+ * @retval AVERROR(ENOMEM) Failed to add a side data entry to the array, or
+ * similar.
+ */
+int av_packet_side_data_to_frame(struct AVFrameSideData ***sd, int *nb_sd,
+ const AVPacketSideData *src, unsigned int
flags);
+
const char *av_packet_side_data_name(enum AVPacketSideDataType type);
/**
diff --git a/libavcodec/version.h b/libavcodec/version.h
index 82a86fe9d9..2618016a83 100644
--- a/libavcodec/version.h
+++ b/libavcodec/version.h
@@ -29,7 +29,7 @@
#include "version_major.h"
-#define LIBAVCODEC_VERSION_MINOR 16
+#define LIBAVCODEC_VERSION_MINOR 17
#define LIBAVCODEC_VERSION_MICRO 100
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
-----------------------------------------------------------------------
Summary of changes:
doc/APIchanges | 3 +
fftools/cmdutils.c | 71 ++++++
fftools/cmdutils.h | 4 +
fftools/ffmpeg.c | 13 +
fftools/ffmpeg.h | 47 +++-
fftools/ffmpeg_demux.c | 161 +++++++++++++
fftools/ffmpeg_enc.c | 17 +-
fftools/ffmpeg_filter.c | 265 +++++++++++++++++----
fftools/ffmpeg_mux_init.c | 53 +++++
fftools/ffmpeg_opt.c | 91 ++++++-
fftools/ffmpeg_sched.c | 23 ++
fftools/ffmpeg_sched.h | 2 +
libavcodec/avcodec.c | 50 ++++
libavcodec/packet.h | 30 +++
libavcodec/version.h | 2 +-
tests/fate/ffmpeg.mak | 13 +
tests/ref/fate/ffmpeg-heif-merge | 6 +
.../fate/{dxtory => ffmpeg-heif-merge-filtergraph} | 4 +-
tests/ref/fate/ffmpeg-heif-merge-mapped | 6 +
19 files changed, 800 insertions(+), 61 deletions(-)
create mode 100644 tests/ref/fate/ffmpeg-heif-merge
copy tests/ref/fate/{dxtory => ffmpeg-heif-merge-filtergraph} (53%)
create mode 100644 tests/ref/fate/ffmpeg-heif-merge-mapped
hooks/post-receive
--
_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]