Search code examples
c++ffmpeg

Dynamic ffmpeg crop, scale & encoding code seems to break when the crop size changes


The following code works perfectly as long as I only move the crop rectangle, however as soon as I change its size I no longer get frames out of my filter (av_buffersink_get_frame returns -11). It's crazy, even after the size changes, if it eventually changes to the original size that frame will go through, then it will go back to no longer providing frames.

Would anyone happen to know what I'm doing wrong?

My filter setup (note the crop & scale combination, it should (I think?) scale whatever I crop to the output video size):

// buffer source -> buffer sink setup
auto args = std::format("video_size={}x{}:pix_fmt={}:time_base={}/{}:pixel_aspect={}/{}",
    inputCodecContext->width, inputCodecContext->height, (int)inputCodecContext->pix_fmt,
    inputCodecContext->pkt_timebase.num, inputCodecContext->pkt_timebase.den,
    inputCodecContext->sample_aspect_ratio.num, inputCodecContext->sample_aspect_ratio.den);

AVFilterContext* buffersrc_ctx = nullptr, * buffersink_ctx = nullptr;
check_av_result(avfilter_graph_create_filter(&buffersrc_ctx, bufferSource, "in",
    args.c_str(), nullptr, &*filterGraph));
check_av_result(avfilter_graph_create_filter(&buffersink_ctx, bufferSink, "out",
    nullptr, nullptr, &*filterGraph));
check_av_result(av_opt_set_bin(buffersink_ctx, "pix_fmts",
    (uint8_t*)&outputCodecContext->pix_fmt, sizeof(outputCodecContext->pix_fmt), AV_OPT_SEARCH_CHILDREN));

// filter command setup
auto filterSpec = std::format("crop,scale={}:{},setsar=1:1", outputCodecContext->width, outputCodecContext->height);

check_av_result(avfilter_graph_parse_ptr(&*filterGraph, filterSpec.c_str(), &filterInputs, &filterOutputs, nullptr));
check_av_result(avfilter_graph_config(&*filterGraph, nullptr));

Frame cropping:

check_av_result(avfilter_graph_send_command(&*filterGraph, "crop", "x", std::to_string(cropRectangle.CenterX() - cropRectangle.Width() / 2).c_str(), nullptr, 0, 0));
check_av_result(avfilter_graph_send_command(&*filterGraph, "crop", "y", std::to_string(cropRectangle.CenterY() - cropRectangle.Height() / 2).c_str(), nullptr, 0, 0));
check_av_result(avfilter_graph_send_command(&*filterGraph, "crop", "w", std::to_string(cropRectangle.Width()).c_str(), nullptr, 0, 0));
check_av_result(avfilter_graph_send_command(&*filterGraph, "crop", "h", std::to_string(cropRectangle.Height()).c_str(), nullptr, 0, 0));

// push the decoded frame into the filter graph
check_av_result(av_buffersrc_add_frame_flags(buffersrc_ctx, &*inputFrame, 0));

// pull filtered frames from the filter graph
while (1)
{
    ret = av_buffersink_get_frame(buffersink_ctx, &*filteredFrame);
    if (ret < 0)
    {
        // if no more frames, rewrite the code to 0 to show it as normal completion
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            ret = 0;
        break;
    }

    // write the filtered frame to the output file 
    // [...]
}

I also set the output video size before creating the file, and it is obeyed as expected:

outputCodecContext->width = (int)output.PixelSize().Width;
outputCodecContext->height = (int)output.PixelSize().Height;

Solution

  • So as it turns out, unless I'm very much mistaking, the bug is in ffmpeg itself, not in my code. Specifically the code that checks if the frame shape has changed to reinitialize the software scaler is broken.

    If anyone else needs this, one quick fix is to just disable the checker and always rebuild the scaler (which is fine in my case, since almost every frame has a different size anyway):

        diff --git a/libavfilter/vf_scale.c b/libavfilter/vf_scale.c
        index 23335ce..8617d45 100644
        --- a/libavfilter/vf_scale.c
        +++ b/libavfilter/vf_scale.c
        @@ -724,7 +724,8 @@ static int scale_frame(AVFilterLink *link, AVFrame *in, AVFrame **frame_out)
             if (in->colorspace == AVCOL_SPC_YCGCO)
                 av_log(link->dst, AV_LOG_WARNING, "Detected unsupported YCgCo colorspace.\n");
         
        -    frame_changed = in->width  != link->w ||
        +    frame_changed = 1 ||
        +                    in->width  != link->w ||
                             in->height != link->h ||
                             in->format != link->format ||
                             in->sample_aspect_ratio.den != link->sample_aspect_ratio.den ||