diff mbox series

[FFmpeg-devel,v2,5/5] fftools/ffmpeg: support applying container level cropping

Message ID 20230725170939.3285-1-jamrial@gmail.com
State New
Headers show
Series None | expand

Commit Message

James Almer July 25, 2023, 5:09 p.m. UTC
Signed-off-by: James Almer <jamrial@gmail.com>
---
Now inserting a filter into the graph.

 fftools/ffmpeg.h        |  3 +++
 fftools/ffmpeg_demux.c  |  6 ++++++
 fftools/ffmpeg_enc.c    | 19 +++++++++++--------
 fftools/ffmpeg_filter.c | 22 ++++++++++++++++++++++
 fftools/ffmpeg_opt.c    |  3 +++
 5 files changed, 45 insertions(+), 8 deletions(-)

Comments

Tomas Härdin July 26, 2023, 9:42 p.m. UTC | #1
tis 2023-07-25 klockan 14:09 -0300 skrev James Almer:
> Signed-off-by: James Almer <jamrial@gmail.com>
> ---
> Now inserting a filter into the graph.

This looks useful for MXF

> +    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
> +                          OPT_EXPERT |
> OPT_INPUT,                                { .off =
> OFFSET(apply_cropping) },
> +        "Apply frame cropping instead of exporting it" },

Hm. Can this be applied automatically for ffplay? When transcoding I
expect the typical use case is to not crop and to carry the metadata
over. MXF -> MOV for example. But when playing I expect one just wants
to see the display rectangle.

/Tomas
James Almer July 26, 2023, 10:11 p.m. UTC | #2
On 7/26/2023 6:42 PM, Tomas Härdin wrote:
> tis 2023-07-25 klockan 14:09 -0300 skrev James Almer:
>> Signed-off-by: James Almer <jamrial@gmail.com>
>> ---
>> Now inserting a filter into the graph.
> 
> This looks useful for MXF
> 
>> +    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
>> +                          OPT_EXPERT |
>> OPT_INPUT,                                { .off =
>> OFFSET(apply_cropping) },
>> +        "Apply frame cropping instead of exporting it" },
> 
> Hm. Can this be applied automatically for ffplay? When transcoding I
> expect the typical use case is to not crop and to carry the metadata
> over. MXF -> MOV for example. But when playing I expect one just wants
> to see the display rectangle.

You want it to be disabled by default on ffmpeg but enabled in ffplay?

For the latter, something like:

> diff --git a/fftools/ffplay.c b/fftools/ffplay.c
> index 5212ad053e..217fc3e45a 100644
> --- a/fftools/ffplay.c
> +++ b/fftools/ffplay.c
> @@ -36,6 +36,7 @@
>  #include "libavutil/eval.h"
>  #include "libavutil/mathematics.h"
>  #include "libavutil/pixdesc.h"
> +#include "libavutil/intreadwrite.h"
>  #include "libavutil/imgutils.h"
>  #include "libavutil/dict.h"
>  #include "libavutil/fifo.h"
> @@ -346,6 +347,7 @@ static const char **vfilters_list = NULL;
>  static int nb_vfilters = 0;
>  static char *afilters = NULL;
>  static int autorotate = 1;
> +static int apply_cropping = 1;
>  static int find_stream_info = 1;
>  static int filter_nbthreads = 0;
> 
> @@ -1922,6 +1924,27 @@ static int configure_video_filters(AVFilterGraph *graph, VideoState *is, const c
>          }
>      }
> 
> +    if (apply_cropping) {
> +        size_t cropping_size;
> +        uint8_t *cropping = av_stream_get_side_data(is->video_st, AV_PKT_DATA_FRAME_CROPPING, &cropping_size);
> +
> +        if (cropping && cropping_size == sizeof(uint32_t) * 4) {
> +            char crop_buf[64];
> +            int top    = AV_RL32(cropping +  0);
> +            int bottom = AV_RL32(cropping +  4);
> +            int left   = AV_RL32(cropping +  8);
> +            int right  = AV_RL32(cropping + 12);
> +
> +            if (top < 0 || bottom < 0 || left < 0 || right < 0)  {
> +                ret = AVERROR(EINVAL);
> +                goto fail;
> +            }
> +
> +            snprintf(crop_buf, sizeof(crop_buf), "w=iw-%d-%d:h=ih-%d-%d", left, right, top, bottom);
> +            INSERT_FILT("crop", crop_buf);
> +        }
> +    }
> +
>      if ((ret = configure_filtergraph(graph, vfilters, filt_src, last_filter)) < 0)
>          goto fail;
> 
> @@ -3593,6 +3616,7 @@ static const OptionDef options[] = {
>      { "scodec", HAS_ARG | OPT_STRING | OPT_EXPERT, { &subtitle_codec_name }, "force subtitle decoder", "decoder_name" },
>      { "vcodec", HAS_ARG | OPT_STRING | OPT_EXPERT, {    &video_codec_name }, "force video decoder",    "decoder_name" },
>      { "autorotate", OPT_BOOL, { &autorotate }, "automatically rotate video", "" },
> +    { "apply_cropping", OPT_BOOL, { &apply_cropping }, "apply frame cropping", "" },
>      { "find_stream_info", OPT_BOOL | OPT_INPUT | OPT_EXPERT, { &find_stream_info },
>          "read and decode the streams to fill missing information with heuristics" },
>      { "filter_threads", HAS_ARG | OPT_INT | OPT_EXPERT, { &filter_nbthreads }, "number of filter threads per graph" },

Would do it, but i don't know if this affects the AVCodecContext option 
of the same name too or not (to apply or not bitstream level cropping, 
like the h264 one, which is obviously enabled by default).

To have it disabled on ffmpeg by default, i think the following would 
work (on top of this patch):

> diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c
> index 1209cf2046..a37c136cf9 100644
> --- a/fftools/ffmpeg_demux.c
> +++ b/fftools/ffmpeg_demux.c
> @@ -1086,10 +1086,11 @@ static int ist_add(const OptionsContext *o, Demuxer *d, AVStream *st)
>      ist->autorotate = 1;
>      MATCH_PER_STREAM_OPT(autorotate, i, ist->autorotate, ic, st);
> 
> -    ist->apply_cropping = 1;
> +    ist->apply_cropping = -1;
>      MATCH_PER_STREAM_OPT(apply_cropping, i, ist->apply_cropping, ic, st);
> 
> -    av_dict_set_int(&o->g->codec_opts, "apply_cropping", ist->apply_cropping, 0);
> +    if (ist->apply_cropping >= 0)
> +        av_dict_set_int(&o->g->codec_opts, "apply_cropping", ist->apply_cropping, 0);
> 
>      MATCH_PER_STREAM_OPT(codec_tags, str, codec_tag, ic, st);
>      if (codec_tag) {
> diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
> index 8cadb4732c..5df52ef718 100644
> --- a/fftools/ffmpeg_filter.c
> +++ b/fftools/ffmpeg_filter.c
> @@ -1419,7 +1419,7 @@ static int configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter,
>              return ret;
>      }
> 
> -    if (ist->apply_cropping && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
> +    if (ist->apply_cropping > 0 && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
>          size_t cropping_size;
>          uint8_t *cropping = av_stream_get_side_data(ist->st, AV_PKT_DATA_FRAME_CROPPING, &cropping_size);
> 

And actually work fine with the AVCodecContext option.
Tomas Härdin July 27, 2023, 11:07 a.m. UTC | #3
ons 2023-07-26 klockan 19:11 -0300 skrev James Almer:
> On 7/26/2023 6:42 PM, Tomas Härdin wrote:
> > tis 2023-07-25 klockan 14:09 -0300 skrev James Almer:
> > > Signed-off-by: James Almer <jamrial@gmail.com>
> > > ---
> > > Now inserting a filter into the graph.
> > 
> > This looks useful for MXF
> > 
> > > +    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
> > > +                          OPT_EXPERT |
> > > OPT_INPUT,                                { .off =
> > > OFFSET(apply_cropping) },
> > > +        "Apply frame cropping instead of exporting it" },
> > 
> > Hm. Can this be applied automatically for ffplay? When transcoding
> > I
> > expect the typical use case is to not crop and to carry the
> > metadata
> > over. MXF -> MOV for example. But when playing I expect one just
> > wants
> > to see the display rectangle.
> 
> You want it to be disabled by default on ffmpeg but enabled in
> ffplay?

Yeah that seems like it'd be most user friendly. Then 3rd party
developers can look at ffplay to see how to apply it in their own
players. mpv and vlc come to mind

> For the latter, something like:
> 
> > diff --git a/fftools/ffplay.c b/fftools/ffplay.c
> > index 5212ad053e..217fc3e45a 100644
> > --- a/fftools/ffplay.c
> > +++ b/fftools/ffplay.c
> > @@ -36,6 +36,7 @@
> >  #include "libavutil/eval.h"
> >  #include "libavutil/mathematics.h"
> >  #include "libavutil/pixdesc.h"
> > +#include "libavutil/intreadwrite.h"
> >  #include "libavutil/imgutils.h"
> >  #include "libavutil/dict.h"
> >  #include "libavutil/fifo.h"
> > @@ -346,6 +347,7 @@ static const char **vfilters_list = NULL;
> >  static int nb_vfilters = 0;
> >  static char *afilters = NULL;
> >  static int autorotate = 1;
> > +static int apply_cropping = 1;
> >  static int find_stream_info = 1;
> >  static int filter_nbthreads = 0;
> > 
> > @@ -1922,6 +1924,27 @@ static int
> > configure_video_filters(AVFilterGraph *graph, VideoState *is, const
> > c
> >          }
> >      }
> > 
> > +    if (apply_cropping) {
> > +        size_t cropping_size;
> > +        uint8_t *cropping = av_stream_get_side_data(is->video_st,
> > AV_PKT_DATA_FRAME_CROPPING, &cropping_size);
> > +
> > +        if (cropping && cropping_size == sizeof(uint32_t) * 4) {
> > +            char crop_buf[64];
> > +            int top    = AV_RL32(cropping +  0);
> > +            int bottom = AV_RL32(cropping +  4);
> > +            int left   = AV_RL32(cropping +  8);
> > +            int right  = AV_RL32(cropping + 12);
> > +
> > +            if (top < 0 || bottom < 0 || left < 0 || right < 0)  {
> > +                ret = AVERROR(EINVAL);
> > +                goto fail;
> > +            }
> > +
> > +            snprintf(crop_buf, sizeof(crop_buf), "w=iw-%d-%d:h=ih-
> > %d-%d", left, right, top, bottom);
> > +            INSERT_FILT("crop", crop_buf);
> > +        }
> > +    }
> > +
> >      if ((ret = configure_filtergraph(graph, vfilters, filt_src,
> > last_filter)) < 0)
> >          goto fail;
> > 
> > @@ -3593,6 +3616,7 @@ static const OptionDef options[] = {
> >      { "scodec", HAS_ARG | OPT_STRING | OPT_EXPERT, {
> > &subtitle_codec_name }, "force subtitle decoder", "decoder_name" },
> >      { "vcodec", HAS_ARG | OPT_STRING | OPT_EXPERT, {   
> > &video_codec_name }, "force video decoder",    "decoder_name" },
> >      { "autorotate", OPT_BOOL, { &autorotate }, "automatically
> > rotate video", "" },
> > +    { "apply_cropping", OPT_BOOL, { &apply_cropping }, "apply
> > frame cropping", "" },
> >      { "find_stream_info", OPT_BOOL | OPT_INPUT | OPT_EXPERT, {
> > &find_stream_info },
> >          "read and decode the streams to fill missing information
> > with heuristics" },
> >      { "filter_threads", HAS_ARG | OPT_INT | OPT_EXPERT, {
> > &filter_nbthreads }, "number of filter threads per graph" },
> 
> Would do it, but i don't know if this affects the AVCodecContext
> option 
> of the same name too or not (to apply or not bitstream level
> cropping, 
> like the h264 one, which is obviously enabled by default).

Right, there's multiple levels of cropping

> To have it disabled on ffmpeg by default, i think the following would
> work (on top of this patch):
> 
> > diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c
> > index 1209cf2046..a37c136cf9 100644
> > --- a/fftools/ffmpeg_demux.c
> > +++ b/fftools/ffmpeg_demux.c
> > @@ -1086,10 +1086,11 @@ static int ist_add(const OptionsContext *o,
> > Demuxer *d, AVStream *st)
> >      ist->autorotate = 1;
> >      MATCH_PER_STREAM_OPT(autorotate, i, ist->autorotate, ic, st);
> > 
> > -    ist->apply_cropping = 1;
> > +    ist->apply_cropping = -1;
> >      MATCH_PER_STREAM_OPT(apply_cropping, i, ist->apply_cropping,
> > ic, st);
> > 
> > -    av_dict_set_int(&o->g->codec_opts, "apply_cropping", ist-
> > >apply_cropping, 0);
> > +    if (ist->apply_cropping >= 0)
> > +        av_dict_set_int(&o->g->codec_opts, "apply_cropping", ist-
> > >apply_cropping, 0);
> > 
> >      MATCH_PER_STREAM_OPT(codec_tags, str, codec_tag, ic, st);
> >      if (codec_tag) {
> > diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
> > index 8cadb4732c..5df52ef718 100644
> > --- a/fftools/ffmpeg_filter.c
> > +++ b/fftools/ffmpeg_filter.c
> > @@ -1419,7 +1419,7 @@ static int
> > configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter,
> >              return ret;
> >      }
> > 
> > -    if (ist->apply_cropping && !(desc->flags &
> > AV_PIX_FMT_FLAG_HWACCEL)) {
> > +    if (ist->apply_cropping > 0 && !(desc->flags &
> > AV_PIX_FMT_FLAG_HWACCEL)) {
> >          size_t cropping_size;
> >          uint8_t *cropping = av_stream_get_side_data(ist->st,
> > AV_PKT_DATA_FRAME_CROPPING, &cropping_size);
> > 
> 
> And actually work fine with the AVCodecContext option.

What would be even neater is if cropping were applied automatically
when the container doesn't support such metadata. Possibly with a
message saying "container does not support display rectangle metadata.
applying cropping automatically. use -disable_cropping to disable". And
a corresponding -force_cropping. Or maybe that's too confusing? My
thinking here is that it should Just Work. If I convert an MXF file
with VBI data in the video essence, to AVI which does not have display
rectangle metadata, then I don't want to see the VBI crap in the output

/Tomas
Anton Khirnov July 27, 2023, 11:13 a.m. UTC | #4
Quoting Tomas Härdin (2023-07-26)
> tis 2023-07-25 klockan 14:09 -0300 skrev James Almer:
> > Signed-off-by: James Almer <jamrial@gmail.com>
> > ---
> > Now inserting a filter into the graph.
> 
> This looks useful for MXF
> 
> > +    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
> > +                          OPT_EXPERT |
> > OPT_INPUT,                                { .off =
> > OFFSET(apply_cropping) },
> > +        "Apply frame cropping instead of exporting it" },
> 
> Hm. Can this be applied automatically for ffplay? When transcoding I
> expect the typical use case is to not crop and to carry the metadata
> over.

Why? We do apply decoder cropping by default. There's also no guarantee
that your container will be able to write it, so it seems better to
apply it by default.
James Almer July 27, 2023, 11:59 a.m. UTC | #5
On 7/27/2023 8:13 AM, Anton Khirnov wrote:
> Quoting Tomas Härdin (2023-07-26)
>> tis 2023-07-25 klockan 14:09 -0300 skrev James Almer:
>>> Signed-off-by: James Almer <jamrial@gmail.com>
>>> ---
>>> Now inserting a filter into the graph.
>>
>> This looks useful for MXF
>>
>>> +    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
>>> +                          OPT_EXPERT |
>>> OPT_INPUT,                                { .off =
>>> OFFSET(apply_cropping) },
>>> +        "Apply frame cropping instead of exporting it" },
>>
>> Hm. Can this be applied automatically for ffplay? When transcoding I
>> expect the typical use case is to not crop and to carry the metadata
>> over.
> 
> Why? We do apply decoder cropping by default. There's also no guarantee
> that your container will be able to write it, so it seems better to
> apply it by default.

I agree. In a transcoding scenario you want to apply the container level 
cropping since it's defining a subrectangle with the actual content 
meant for display, so why force the encoder handle pixels that were 
meant to be discarded to being with, potentially ruining encoding 
quality for neighboring pixels?

For codec copy scenarios though, the side data is going to be copied, so 
Tomas' idea of having muxers report they support writing it is good 
either way.
Tomas Härdin July 31, 2023, 3:53 p.m. UTC | #6
tor 2023-07-27 klockan 08:59 -0300 skrev James Almer:
> On 7/27/2023 8:13 AM, Anton Khirnov wrote:
> > Quoting Tomas Härdin (2023-07-26)
> > > tis 2023-07-25 klockan 14:09 -0300 skrev James Almer:
> > > > Signed-off-by: James Almer <jamrial@gmail.com>
> > > > ---
> > > > Now inserting a filter into the graph.
> > > 
> > > This looks useful for MXF
> > > 
> > > > +    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
> > > > +                          OPT_EXPERT |
> > > > OPT_INPUT,                                { .off =
> > > > OFFSET(apply_cropping) },
> > > > +        "Apply frame cropping instead of exporting it" },
> > > 
> > > Hm. Can this be applied automatically for ffplay? When
> > > transcoding I
> > > expect the typical use case is to not crop and to carry the
> > > metadata
> > > over.
> > 
> > Why? We do apply decoder cropping by default. There's also no
> > guarantee
> > that your container will be able to write it, so it seems better to
> > apply it by default.

Not necessarily. Doing this by default may break some downstream
projects. The relevant metadata must be deleted if this is done, so
that the cropping isn't done twice when you get to playout.

> I agree. In a transcoding scenario you want to apply the container
> level 
> cropping since it's defining a subrectangle with the actual content 
> meant for display, so why force the encoder handle pixels that were 
> meant to be discarded to being with, potentially ruining encoding 
> quality for neighboring pixels?
> 
> For codec copy scenarios though, the side data is going to be copied,
> so 
> Tomas' idea of having muxers report they support writing it is good 
> either way.

My main concern is not losing pixels if we can avoid it, even if those
pixels are invisible. On the other hand, when transcoding, we could go
with always cropping unless the user requests otherwise. This has the
benefit of essence dimensions not changing with container. Also less
work for the encoder. But again, this is a behavior change that may
break things downstream.

Basically what I'm suggesting is that ffplay behave as playout. We
could have ffmpeg behave similarly but we should keep in mind this may
break some workflows.

/Tomas
Cosmin Stejerean Aug. 1, 2023, 6:51 p.m. UTC | #7
On Jul 27, 2023, at 4:13 AM, Anton Khirnov <anton@khirnov.net> wrote:

Quoting Tomas Härdin (2023-07-26)
tis 2023-07-25 klockan 14:09 -0300 skrev James Almer:
Signed-off-by: James Almer <jamrial@gmail.com>
---
Now inserting a filter into the graph.

This looks useful for MXF

+    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
+                          OPT_EXPERT |
OPT_INPUT,                                { .off =
OFFSET(apply_cropping) },
+        "Apply frame cropping instead of exporting it" },

Hm. Can this be applied automatically for ffplay? When transcoding I
expect the typical use case is to not crop and to carry the metadata
over.

Why? We do apply decoder cropping by default. There's also no guarantee
that your container will be able to write it, so it seems better to
apply it by default.


I agree. I see this similar to rotation. And edit lists.

For remuxing we want to keep the metadata and have the muxer write it, but if we're going to transcode anyway we should simplify the stream (apply rotation, apply cropping, keep only visible frames, etc) and write out something as simple as possible. Anyone that doesn't want this can opt out of it like opting out of autorotation.

Not doing this means compatibility is worse when downstream players inevitably don't handle something properly (edit lists are still a mess in terms of compatibility for example). And of potentially displaying content that the user did not intend to be displayed.

- Cosmin
diff mbox series

Patch

diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h
index d53181e427..bb6d510f10 100644
--- a/fftools/ffmpeg.h
+++ b/fftools/ffmpeg.h
@@ -154,6 +154,8 @@  typedef struct OptionsContext {
     int        nb_hwaccel_output_formats;
     SpecifierOpt *autorotate;
     int        nb_autorotate;
+    SpecifierOpt *apply_cropping;
+    int        nb_apply_cropping;
 
     /* output options */
     StreamMap *stream_maps;
@@ -347,6 +349,7 @@  typedef struct InputStream {
     int top_field_first;
 
     int autorotate;
+    int apply_cropping;
 
     int fix_sub_duration;
 
diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c
index 48edbd7f6b..1209cf2046 100644
--- a/fftools/ffmpeg_demux.c
+++ b/fftools/ffmpeg_demux.c
@@ -48,6 +48,7 @@  static const char *const opt_name_hwaccels[]                  = {"hwaccel", NULL
 static const char *const opt_name_hwaccel_devices[]           = {"hwaccel_device", NULL};
 static const char *const opt_name_hwaccel_output_formats[]    = {"hwaccel_output_format", NULL};
 static const char *const opt_name_autorotate[]                = {"autorotate", NULL};
+static const char *const opt_name_apply_cropping[]            = {"apply_cropping", NULL};
 static const char *const opt_name_display_rotations[]         = {"display_rotation", NULL};
 static const char *const opt_name_display_hflips[]            = {"display_hflip", NULL};
 static const char *const opt_name_display_vflips[]            = {"display_vflip", NULL};
@@ -1085,6 +1086,11 @@  static int ist_add(const OptionsContext *o, Demuxer *d, AVStream *st)
     ist->autorotate = 1;
     MATCH_PER_STREAM_OPT(autorotate, i, ist->autorotate, ic, st);
 
+    ist->apply_cropping = 1;
+    MATCH_PER_STREAM_OPT(apply_cropping, i, ist->apply_cropping, ic, st);
+
+    av_dict_set_int(&o->g->codec_opts, "apply_cropping", ist->apply_cropping, 0);
+
     MATCH_PER_STREAM_OPT(codec_tags, str, codec_tag, ic, st);
     if (codec_tag) {
         uint32_t tag = strtol(codec_tag, &next, 0);
diff --git a/fftools/ffmpeg_enc.c b/fftools/ffmpeg_enc.c
index 96424272bf..cd30986344 100644
--- a/fftools/ffmpeg_enc.c
+++ b/fftools/ffmpeg_enc.c
@@ -448,14 +448,17 @@  int enc_open(OutputStream *ost, AVFrame *frame)
         int i;
         for (i = 0; i < ist->st->nb_side_data; i++) {
             AVPacketSideData *sd = &ist->st->side_data[i];
-            if (sd->type != AV_PKT_DATA_CPB_PROPERTIES) {
-                uint8_t *dst = av_stream_new_side_data(ost->st, sd->type, sd->size);
-                if (!dst)
-                    return AVERROR(ENOMEM);
-                memcpy(dst, sd->data, sd->size);
-                if (ist->autorotate && sd->type == AV_PKT_DATA_DISPLAYMATRIX)
-                    av_display_rotation_set((int32_t *)dst, 0);
-            }
+            uint8_t *dst;
+            if (sd->type == AV_PKT_DATA_CPB_PROPERTIES)
+                continue;
+            if (ist->apply_cropping && sd->type == AV_PKT_DATA_FRAME_CROPPING)
+                continue;
+            dst = av_stream_new_side_data(ost->st, sd->type, sd->size);
+            if (!dst)
+                return AVERROR(ENOMEM);
+            memcpy(dst, sd->data, sd->size);
+            if (ist->autorotate && sd->type == AV_PKT_DATA_DISPLAYMATRIX)
+                av_display_rotation_set((int32_t *)dst, 0);
         }
     }
 
diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index 925b5116cc..8cadb4732c 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -31,6 +31,7 @@ 
 #include "libavutil/bprint.h"
 #include "libavutil/channel_layout.h"
 #include "libavutil/display.h"
+#include "libavutil/intreadwrite.h"
 #include "libavutil/opt.h"
 #include "libavutil/pixdesc.h"
 #include "libavutil/pixfmt.h"
@@ -1418,6 +1419,27 @@  static int configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter,
             return ret;
     }
 
+    if (ist->apply_cropping && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
+        size_t cropping_size;
+        uint8_t *cropping = av_stream_get_side_data(ist->st, AV_PKT_DATA_FRAME_CROPPING, &cropping_size);
+
+        if (cropping && cropping_size == sizeof(uint32_t) * 4) {
+            char crop_buf[64];
+            int top    = AV_RL32(cropping +  0);
+            int bottom = AV_RL32(cropping +  4);
+            int left   = AV_RL32(cropping +  8);
+            int right  = AV_RL32(cropping + 12);
+
+            if (top < 0 || bottom < 0 || left < 0 || right < 0)
+                return AVERROR(EINVAL);
+
+            snprintf(crop_buf, sizeof(crop_buf), "w=iw-%d-%d:h=ih-%d-%d", left, right, top, bottom);
+            ret = insert_filter(&last_filter, &pad_idx, "crop", crop_buf);
+            if (ret < 0)
+                return ret;
+        }
+    }
+
     snprintf(name, sizeof(name), "trim_in_%d_%d",
              ist->file_index, ist->index);
     if (copy_ts) {
diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c
index dc6044120a..30bb919cb6 100644
--- a/fftools/ffmpeg_opt.c
+++ b/fftools/ffmpeg_opt.c
@@ -1733,6 +1733,9 @@  const OptionDef options[] = {
     { "autorotate",       HAS_ARG | OPT_BOOL | OPT_SPEC |
                           OPT_EXPERT | OPT_INPUT,                                { .off = OFFSET(autorotate) },
         "automatically insert correct rotate filters" },
+    { "apply_cropping",   HAS_ARG | OPT_BOOL | OPT_SPEC |
+                          OPT_EXPERT | OPT_INPUT,                                { .off = OFFSET(apply_cropping) },
+        "Apply frame cropping instead of exporting it" },
     { "autoscale",        HAS_ARG | OPT_BOOL | OPT_SPEC |
                           OPT_EXPERT | OPT_OUTPUT,                               { .off = OFFSET(autoscale) },
         "automatically insert a scale filter at the end of the filter graph" },