diff mbox

[FFmpeg-devel] avfilter/f_cue: add cue and acue filters

Message ID 20180825183501.32385-1-cus@passwd.hu
State New
Headers show

Commit Message

Marton Balint Aug. 25, 2018, 6:35 p.m. UTC
To delay filtering until a given wallclock timestamp.

Signed-off-by: Marton Balint <cus@passwd.hu>
---
 doc/filters.texi         |  36 ++++++++++
 libavfilter/Makefile     |   2 +
 libavfilter/allfilters.c |   2 +
 libavfilter/f_cue.c      | 182 +++++++++++++++++++++++++++++++++++++++++++++++
 libavfilter/version.h    |   2 +-
 5 files changed, 223 insertions(+), 1 deletion(-)
 create mode 100644 libavfilter/f_cue.c

Comments

Nicolas George Aug. 25, 2018, 7:55 p.m. UTC | #1
Marton Balint (2018-08-25):
> +    FFFrameQueue queue;

There is already a frame queue in the link, it would be much better to
use it rather than having a second one. For that, you need to use a
"activate" callback instead of filter_frame, the working is mostly the
same for simple filters like that.

No time to look at the patch in more details now.

Regards,
Marton Balint Aug. 25, 2018, 8:41 p.m. UTC | #2
On Sat, 25 Aug 2018, Nicolas George wrote:

> Marton Balint (2018-08-25):
>> +    FFFrameQueue queue;
>
> There is already a frame queue in the link, it would be much better to
> use it rather than having a second one. For that, you need to use a
> "activate" callback instead of filter_frame, the working is mostly the
> same for simple filters like that.

I am already using activate. I have no idea how to access the inlink fifo 
to get the timestamp of the first and last frame in it.

Regards,
Marton
Marton Balint Sept. 1, 2018, 8:24 p.m. UTC | #3
On Sat, 25 Aug 2018, Marton Balint wrote:

>
>
> On Sat, 25 Aug 2018, Nicolas George wrote:
>
>> Marton Balint (2018-08-25):
>>> +    FFFrameQueue queue;
>>
>> There is already a frame queue in the link, it would be much better to
>> use it rather than having a second one. For that, you need to use a
>> "activate" callback instead of filter_frame, the working is mostly the
>> same for simple filters like that.
>
> I am already using activate. I have no idea how to access the inlink fifo 
> to get the timestamp of the first and last frame in it.

As far as I see directly accessing the inlink fifo is prohibited by 
design. So please let me know if there is a way I missed to access the 
timestamps in the inlink fifo... Otherwise I will just push this as is.

Thanks,
Marton
Bodecs Bela Sept. 1, 2018, 9:48 p.m. UTC | #4
Hi Balint,


2018.08.25. 20:35 keltezéssel, Marton Balint írta:
> To delay filtering until a given wallclock timestamp.
>
> Signed-off-by: Marton Balint <cus@passwd.hu>
> ---
>   doc/filters.texi         |  36 ++++++++++
>   libavfilter/Makefile     |   2 +
>   libavfilter/allfilters.c |   2 +
>   libavfilter/f_cue.c      | 182 +++++++++++++++++++++++++++++++++++++++++++++++
>   libavfilter/version.h    |   2 +-
>   5 files changed, 223 insertions(+), 1 deletion(-)
>   create mode 100644 libavfilter/f_cue.c
>
> diff --git a/doc/filters.texi b/doc/filters.texi
> index 32c95b591c..79eec0c808 100644
> --- a/doc/filters.texi
> +++ b/doc/filters.texi
> @@ -551,6 +551,11 @@ Set LFO range.
>   Set LFO rate.
>   @end table
>   
> +@section acue
> +
> +Delay audio filtering until a given wallclock timestamp. See the @ref{cue}
> +filter.
> +
>   @section adeclick
>   Remove impulsive noise from input audio.
>   
> @@ -6987,6 +6992,37 @@ indicates 'never reset', and returns the largest area encountered during
>   playback.
>   @end table
>   
> +@anchor{cue}
> +@section cue
> +
> +Delay video filtering until a given wallclock timestamp. The filter first
> +passes on @option{preroll} amount of frames, then it buffers at most
> +@option{buffer} amount of frames and waits for the cue. After reaching the cue
> +it forwards the buffered frames and also any subsequent frames coming in its
> +input.
> +
> +The filter can be used synchronize the output of multiple ffmpeg processes for
> +realtime output devices like decklink. By putting the delay in the filtering
> +chain and pre-buffering frames the process can pass on data to output almost
> +immediately after the target wallclock timestamp is reached.
> +
> +Perfect frame accuracy cannot be guaranteed, but the result is good enough for
> +some use cases.
just for my curiousity, will you please give an example/use_case how to 
use these filters?

> +
> +@table @option
> +
> +@item cue
> +The cue timestamp expressed in a UNIX timestamp in microseconds. Default is 0.
> +
> +@item preroll
> +The duration of content to pass on as preroll expressed in seconds. Default is 0.
> +
> +@item buffer
> +The maximum duration of content to buffer before waiting for the cue expressed
> +in seconds. Default is 0.
> +
> +@end table
> +
>   @anchor{curves}
>   @section curves
>   
> diff --git a/libavfilter/Makefile b/libavfilter/Makefile
> index e5d3a57af7..37a06e0ec0 100644
> --- a/libavfilter/Makefile
> +++ b/libavfilter/Makefile
> @@ -36,6 +36,7 @@ OBJS-$(CONFIG_ACONTRAST_FILTER)              += af_acontrast.o
>   OBJS-$(CONFIG_ACOPY_FILTER)                  += af_acopy.o
>   OBJS-$(CONFIG_ACROSSFADE_FILTER)             += af_afade.o
>   OBJS-$(CONFIG_ACRUSHER_FILTER)               += af_acrusher.o
> +OBJS-$(CONFIG_ACUE_FILTER)                   += f_cue.o
>   OBJS-$(CONFIG_ADECLICK_FILTER)               += af_adeclick.o
>   OBJS-$(CONFIG_ADECLIP_FILTER)                += af_adeclick.o
>   OBJS-$(CONFIG_ADELAY_FILTER)                 += af_adelay.o
> @@ -178,6 +179,7 @@ OBJS-$(CONFIG_COREIMAGE_FILTER)              += vf_coreimage.o
>   OBJS-$(CONFIG_COVER_RECT_FILTER)             += vf_cover_rect.o lavfutils.o
>   OBJS-$(CONFIG_CROP_FILTER)                   += vf_crop.o
>   OBJS-$(CONFIG_CROPDETECT_FILTER)             += vf_cropdetect.o
> +OBJS-$(CONFIG_CUE_FILTER)                    += f_cue.o
>   OBJS-$(CONFIG_CURVES_FILTER)                 += vf_curves.o
>   OBJS-$(CONFIG_DATASCOPE_FILTER)              += vf_datascope.o
>   OBJS-$(CONFIG_DCTDNOIZ_FILTER)               += vf_dctdnoiz.o
> diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
> index 9732ae5345..6c6d0f43f0 100644
> --- a/libavfilter/allfilters.c
> +++ b/libavfilter/allfilters.c
> @@ -27,6 +27,7 @@ extern AVFilter ff_af_abench;
>   extern AVFilter ff_af_acompressor;
>   extern AVFilter ff_af_acontrast;
>   extern AVFilter ff_af_acopy;
> +extern AVFilter ff_af_acue;
>   extern AVFilter ff_af_acrossfade;
>   extern AVFilter ff_af_acrusher;
>   extern AVFilter ff_af_adeclick;
> @@ -167,6 +168,7 @@ extern AVFilter ff_vf_coreimage;
>   extern AVFilter ff_vf_cover_rect;
>   extern AVFilter ff_vf_crop;
>   extern AVFilter ff_vf_cropdetect;
> +extern AVFilter ff_vf_cue;
>   extern AVFilter ff_vf_curves;
>   extern AVFilter ff_vf_datascope;
>   extern AVFilter ff_vf_dctdnoiz;
> diff --git a/libavfilter/f_cue.c b/libavfilter/f_cue.c
> new file mode 100644
> index 0000000000..732b5e218a
> --- /dev/null
> +++ b/libavfilter/f_cue.c
> @@ -0,0 +1,182 @@
> +/*
> + * Copyright (c) 2018 Marton Balint
> + *
> + * This file is part of FFmpeg.
> + *
> + * FFmpeg is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU Lesser General Public License
> + * as published by the Free Software Foundation; either
> + * version 2.1 of the License, or (at your option) any later version.
> + *
> + * FFmpeg is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public License
> + * along with FFmpeg; if not, write to the Free Software Foundation, Inc.,
> + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
> + */
> +
> +#include "libavutil/opt.h"
> +#include "libavutil/time.h"
> +#include "avfilter.h"
> +#include "filters.h"
> +#include "framequeue.h"
> +#include "internal.h"
> +
> +typedef struct CueContext {
> +    const AVClass *class;
> +    int64_t first_pts;
> +    int64_t cue;
> +    int64_t preroll;
> +    int64_t buffer;
> +    int status;
> +    FFFrameQueue queue;
> +} CueContext;
> +
> +static av_cold int init(AVFilterContext *ctx)
> +{
> +    CueContext *s = ctx->priv;
> +    ff_framequeue_init(&s->queue, &ctx->graph->internal->frame_queues);
> +    return 0;
> +}
> +
> +static av_cold void uninit(AVFilterContext *ctx)
> +{
> +    CueContext *s = ctx->priv;
> +    ff_framequeue_free(&s->queue);
> +}
> +
> +static int activate(AVFilterContext *ctx)
> +{
> +    AVFilterLink *inlink = ctx->inputs[0];
> +    AVFilterLink *outlink = ctx->outputs[0];
> +    CueContext *s = ctx->priv;
> +    int64_t pts;
> +    AVFrame *frame = NULL;
> +
> +    FF_FILTER_FORWARD_STATUS_BACK(outlink, inlink);
> +
> +    if (s->status < 3 || s->status == 5) {
> +        int ret = ff_inlink_consume_frame(inlink, &frame);
> +        if (ret < 0)
> +            return ret;
> +        if (frame)
> +            pts = av_rescale_q(frame->pts, inlink->time_base, AV_TIME_BASE_Q);
> +    }
> +
> +    if (!s->status && frame) {
> +        s->first_pts = pts;
> +        s->status++;
> +    }
> +    if (s->status == 1 && frame) {
> +        if (pts - s->first_pts < s->preroll)
> +            return ff_filter_frame(outlink, frame);
> +        s->first_pts = pts;
> +        s->status++;
> +    }
> +    if (s->status == 2 && frame) {
> +        int ret = ff_framequeue_add(&s->queue, frame);
> +        if (ret < 0) {
> +            av_frame_free(&frame);
> +            return ret;
> +        }
> +        frame = NULL;
> +        if (!(pts - s->first_pts < s->buffer && (av_gettime() - s->cue) < 0))
> +            s->status++;
> +    }
> +    if (s->status == 3) {
> +        int64_t diff;
> +        while ((diff = (av_gettime() - s->cue)) < 0)
> +            av_usleep(av_clip(-diff / 2, 100, 1000000));
> +        s->status++;
> +    }
> +    if (s->status == 4) {
> +        if (ff_framequeue_queued_frames(&s->queue))
> +            return ff_filter_frame(outlink, ff_framequeue_take(&s->queue));
> +        s->status++;
> +    }
> +    if (s->status == 5 && frame)
> +        return ff_filter_frame(outlink, frame);
> +
> +    FF_FILTER_FORWARD_STATUS(inlink, outlink);
> +    FF_FILTER_FORWARD_WANTED(outlink, inlink);
> +
> +    return FFERROR_NOT_READY;
> +}
> +
> +#define OFFSET(x) offsetof(CueContext, x)
> +#define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_AUDIO_PARAM | AV_OPT_FLAG_FILTERING_PARAM
> +static const AVOption options[] = {
> +    { "cue", "cue unix timestamp in microseconds", OFFSET(cue), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, FLAGS },
> +    { "preroll", "preroll duration in seconds", OFFSET(preroll), AV_OPT_TYPE_DURATION, { .i64 = 0 }, 0, INT64_MAX, FLAGS },
> +    { "buffer", "buffer duration in seconds", OFFSET(buffer), AV_OPT_TYPE_DURATION, { .i64 = 0 }, 0, INT64_MAX, FLAGS },
> +    { NULL }
> +};
> +
> +#if CONFIG_CUE_FILTER
> +#define cue_options options
> +AVFILTER_DEFINE_CLASS(cue);
> +
> +static const AVFilterPad cue_inputs[] = {
> +    {
> +        .name = "default",
> +        .type = AVMEDIA_TYPE_VIDEO,
> +    },
> +    { NULL }
> +};
> +
> +static const AVFilterPad cue_outputs[] = {
> +    {
> +        .name = "default",
> +        .type = AVMEDIA_TYPE_VIDEO,
> +    },
> +    { NULL }
> +};
> +
> +AVFilter ff_vf_cue = {
> +    .name        = "cue",
> +    .description = NULL_IF_CONFIG_SMALL("Delay filtering to match a cue."),
> +    .priv_size   = sizeof(CueContext),
> +    .priv_class  = &cue_class,
> +    .init        = init,
> +    .uninit      = uninit,
> +    .inputs      = cue_inputs,
> +    .outputs     = cue_outputs,
> +    .activate    = activate,
> +};
> +#endif /* CONFIG_CUE_FILTER */
> +
> +#if CONFIG_ACUE_FILTER
> +#define acue_options options
> +AVFILTER_DEFINE_CLASS(acue);
> +
> +static const AVFilterPad acue_inputs[] = {
> +    {
> +        .name = "default",
> +        .type = AVMEDIA_TYPE_AUDIO,
> +    },
> +    { NULL }
> +};
> +
> +static const AVFilterPad acue_outputs[] = {
> +    {
> +        .name = "default",
> +        .type = AVMEDIA_TYPE_AUDIO,
> +    },
> +    { NULL }
> +};
> +
> +AVFilter ff_af_acue = {
> +    .name        = "acue",
> +    .description = NULL_IF_CONFIG_SMALL("Delay filtering to match a cue."),
> +    .priv_size   = sizeof(CueContext),
> +    .priv_class  = &acue_class,
> +    .init        = init,
> +    .uninit      = uninit,
> +    .inputs      = acue_inputs,
> +    .outputs     = acue_outputs,
> +    .activate    = activate,
> +};
> +#endif /* CONFIG_ACUE_FILTER */
> diff --git a/libavfilter/version.h b/libavfilter/version.h
> index 0ac3a2f3a9..2ff2b6a318 100644
> --- a/libavfilter/version.h
> +++ b/libavfilter/version.h
> @@ -30,7 +30,7 @@
>   #include "libavutil/version.h"
>   
>   #define LIBAVFILTER_VERSION_MAJOR   7
> -#define LIBAVFILTER_VERSION_MINOR  26
> +#define LIBAVFILTER_VERSION_MINOR  27
>   #define LIBAVFILTER_VERSION_MICRO 100
>   
>   #define LIBAVFILTER_VERSION_INT AV_VERSION_INT(LIBAVFILTER_VERSION_MAJOR, \
thank you,

bb
Marton Balint Sept. 6, 2018, 8:20 p.m. UTC | #5
On Sat, 1 Sep 2018, Bodecs Bela wrote:

> Hi Balint,
>
>
> 2018.08.25. 20:35 keltezéssel, Marton Balint írta:
>> To delay filtering until a given wallclock timestamp.
>>
>> Signed-off-by: Marton Balint <cus@passwd.hu>
>> ---
>>   doc/filters.texi         |  36 ++++++++++
>>   libavfilter/Makefile     |   2 +
>>   libavfilter/allfilters.c |   2 +
>>   libavfilter/f_cue.c      | 182 +++++++++++++++++++++++++++++++++++++++++++++++
>>   libavfilter/version.h    |   2 +-
>>   5 files changed, 223 insertions(+), 1 deletion(-)
>>   create mode 100644 libavfilter/f_cue.c
>>
>> diff --git a/doc/filters.texi b/doc/filters.texi
>> index 32c95b591c..79eec0c808 100644
>> --- a/doc/filters.texi
>> +++ b/doc/filters.texi
>> @@ -551,6 +551,11 @@ Set LFO range.
>>   Set LFO rate.
>>   @end table
>> 
>> +@section acue
>> +
>> +Delay audio filtering until a given wallclock timestamp. See the @ref{cue}
>> +filter.
>> +
>>   @section adeclick
>>   Remove impulsive noise from input audio.
>> 
>> @@ -6987,6 +6992,37 @@ indicates 'never reset', and returns the largest area encountered during
>>   playback.
>>   @end table
>> 
>> +@anchor{cue}
>> +@section cue
>> +
>> +Delay video filtering until a given wallclock timestamp. The filter first
>> +passes on @option{preroll} amount of frames, then it buffers at most
>> +@option{buffer} amount of frames and waits for the cue. After reaching the cue
>> +it forwards the buffered frames and also any subsequent frames coming in its
>> +input.
>> +
>> +The filter can be used synchronize the output of multiple ffmpeg processes for
>> +realtime output devices like decklink. By putting the delay in the filtering
>> +chain and pre-buffering frames the process can pass on data to output almost
>> +immediately after the target wallclock timestamp is reached.
>> +
>> +Perfect frame accuracy cannot be guaranteed, but the result is good enough for
>> +some use cases.

> just for my curiousity, will you please give an example/use_case how to 
> use these filters?

Here is a shell script:

#!/bin/bash
UNIX_TIMESTAMP=`date +%s`
TS=$((UNIX_TIMESTAMP*1000000 + 2500000))
ffmpeg -nostdin -nostats -stream_loop -1 -i input1.mp4 \
  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (1)" &
ffmpeg -nostdin -nostats -stream_loop -1 -i input2.mp4 \
  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (2)" &
ffmpeg -nostdin -nostats -stream_loop -1 -i input3.mp4 \
  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (3)" &
ffmpeg -nostdin -nostats -stream_loop -1 -i input4.mp4 \
  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (4)" &

We are doing something similar to drive 16 TVs in a studio set 
simultaneously.

Regards,
Marton
Bodecs Bela Sept. 6, 2018, 8:50 p.m. UTC | #6
Hi Balint,


2018.09.06. 22:20 keltezéssel, Marton Balint írta:
>
> On Sat, 1 Sep 2018, Bodecs Bela wrote:
>
>> Hi Balint,
>>
>>
>> 2018.08.25. 20:35 keltezéssel, Marton Balint írta:
>>> To delay filtering until a given wallclock timestamp.
>>>
>>> Signed-off-by: Marton Balint <cus@passwd.hu>
>>> ---
>>>   doc/filters.texi         |  36 ++++++++++
>>>   libavfilter/Makefile     |   2 +
>>>   libavfilter/allfilters.c |   2 +
>>>   libavfilter/f_cue.c      | 182 
>>> +++++++++++++++++++++++++++++++++++++++++++++++
>>>   libavfilter/version.h    |   2 +-
>>>   5 files changed, 223 insertions(+), 1 deletion(-)
>>>   create mode 100644 libavfilter/f_cue.c
>>>
>>> diff --git a/doc/filters.texi b/doc/filters.texi
>>> index 32c95b591c..79eec0c808 100644
>>> --- a/doc/filters.texi
>>> +++ b/doc/filters.texi
>>> @@ -551,6 +551,11 @@ Set LFO range.
>>>   Set LFO rate.
>>>   @end table
>>>
>>> +@section acue
>>> +
>>> +Delay audio filtering until a given wallclock timestamp. See the 
>>> @ref{cue}
>>> +filter.
>>> +
>>>   @section adeclick
>>>   Remove impulsive noise from input audio.
>>>
>>> @@ -6987,6 +6992,37 @@ indicates 'never reset', and returns the 
>>> largest area encountered during
>>>   playback.
>>>   @end table
>>>
>>> +@anchor{cue}
>>> +@section cue
>>> +
>>> +Delay video filtering until a given wallclock timestamp. The filter 
>>> first
>>> +passes on @option{preroll} amount of frames, then it buffers at most
>>> +@option{buffer} amount of frames and waits for the cue. After 
>>> reaching the cue
>>> +it forwards the buffered frames and also any subsequent frames 
>>> coming in its
>>> +input.
>>> +
>>> +The filter can be used synchronize the output of multiple ffmpeg 
>>> processes for
>>> +realtime output devices like decklink. By putting the delay in the 
>>> filtering
>>> +chain and pre-buffering frames the process can pass on data to 
>>> output almost
>>> +immediately after the target wallclock timestamp is reached.
>>> +
>>> +Perfect frame accuracy cannot be guaranteed, but the result is good 
>>> enough for
>>> +some use cases.
>
>> just for my curiousity, will you please give an example/use_case how 
>> to use these filters?
>
> Here is a shell script:
>
> #!/bin/bash
> UNIX_TIMESTAMP=`date +%s`
> TS=$((UNIX_TIMESTAMP*1000000 + 2500000))
> ffmpeg -nostdin -nostats -stream_loop -1 -i input1.mp4 \
>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (1)" &
> ffmpeg -nostdin -nostats -stream_loop -1 -i input2.mp4 \
>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (2)" &
> ffmpeg -nostdin -nostats -stream_loop -1 -i input3.mp4 \
>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (3)" &
> ffmpeg -nostdin -nostats -stream_loop -1 -i input4.mp4 \
>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (4)" &
>
Thank you.

what about to create a new parameter for the filters: relcue/arelcue ?

relcue would be a relative value to current timestamp. This way the 
shell variable will be unneeded.

usage of cue and relcue would be mutally esclusive parameters.

> We are doing something similar to drive 16 TVs in a studio set 
> simultaneously.
>
> Regards,
> Marton
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> http://ffmpeg.org/mailman/listinfo/ffmpeg-devel

bb
Marton Balint Sept. 6, 2018, 9:10 p.m. UTC | #7
On Thu, 6 Sep 2018, Bodecs Bela wrote:

> Hi Balint,
>
>
> 2018.09.06. 22:20 keltezéssel, Marton Balint írta:
>>
>> On Sat, 1 Sep 2018, Bodecs Bela wrote:
>>
>>> Hi Balint,
>>>
>>>
>>> 2018.08.25. 20:35 keltezéssel, Marton Balint írta:
>>>> To delay filtering until a given wallclock timestamp.
>>>>
>>>> Signed-off-by: Marton Balint <cus@passwd.hu>
>>>> ---
>>>>   doc/filters.texi         |  36 ++++++++++
>>>>   libavfilter/Makefile     |   2 +
>>>>   libavfilter/allfilters.c |   2 +
>>>>   libavfilter/f_cue.c      | 182 
>>>> +++++++++++++++++++++++++++++++++++++++++++++++
>>>>   libavfilter/version.h    |   2 +-
>>>>   5 files changed, 223 insertions(+), 1 deletion(-)
>>>>   create mode 100644 libavfilter/f_cue.c
>>>>
>>>> diff --git a/doc/filters.texi b/doc/filters.texi
>>>> index 32c95b591c..79eec0c808 100644
>>>> --- a/doc/filters.texi
>>>> +++ b/doc/filters.texi
>>>> @@ -551,6 +551,11 @@ Set LFO range.
>>>>   Set LFO rate.
>>>>   @end table
>>>>
>>>> +@section acue
>>>> +
>>>> +Delay audio filtering until a given wallclock timestamp. See the 
>>>> @ref{cue}
>>>> +filter.
>>>> +
>>>>   @section adeclick
>>>>   Remove impulsive noise from input audio.
>>>>
>>>> @@ -6987,6 +6992,37 @@ indicates 'never reset', and returns the 
>>>> largest area encountered during
>>>>   playback.
>>>>   @end table
>>>>
>>>> +@anchor{cue}
>>>> +@section cue
>>>> +
>>>> +Delay video filtering until a given wallclock timestamp. The filter 
>>>> first
>>>> +passes on @option{preroll} amount of frames, then it buffers at most
>>>> +@option{buffer} amount of frames and waits for the cue. After 
>>>> reaching the cue
>>>> +it forwards the buffered frames and also any subsequent frames 
>>>> coming in its
>>>> +input.
>>>> +
>>>> +The filter can be used synchronize the output of multiple ffmpeg 
>>>> processes for
>>>> +realtime output devices like decklink. By putting the delay in the 
>>>> filtering
>>>> +chain and pre-buffering frames the process can pass on data to 
>>>> output almost
>>>> +immediately after the target wallclock timestamp is reached.
>>>> +
>>>> +Perfect frame accuracy cannot be guaranteed, but the result is good 
>>>> enough for
>>>> +some use cases.
>>
>>> just for my curiousity, will you please give an example/use_case how 
>>> to use these filters?
>>
>> Here is a shell script:
>>
>> #!/bin/bash
>> UNIX_TIMESTAMP=`date +%s`
>> TS=$((UNIX_TIMESTAMP*1000000 + 2500000))
>> ffmpeg -nostdin -nostats -stream_loop -1 -i input1.mp4 \
>>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (1)" &
>> ffmpeg -nostdin -nostats -stream_loop -1 -i input2.mp4 \
>>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (2)" &
>> ffmpeg -nostdin -nostats -stream_loop -1 -i input3.mp4 \
>>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (3)" &
>> ffmpeg -nostdin -nostats -stream_loop -1 -i input4.mp4 \
>>  -vf "format=uyvy422,cue=$TS:preroll=0.5:buffer=0.5" \
>>  -af "acue=$TS:preroll=0.5:buffer=0.5" -f decklink "DeckLink Duo (4)" &
>>
> Thank you.
>
> what about to create a new parameter for the filters: relcue/arelcue ?
>
> relcue would be a relative value to current timestamp. This way the 
> shell variable will be unneeded.

The shell variable is needed to be able to provide a common start 
timestamp to all the processes. If relative timestamp is used, that kind 
of defeats the purpose of synchronization, because different processes 
will have different delays in opening their inputs, which means variable 
delay in initalizing their filters, so the start time for passing data 
will not be common among processes.

>
> usage of cue and relcue would be mutally esclusive parameters.

Obviously it is possible, can be added later if somebody finds a valid use 
case for it :)

Regards,
Marton
Marton Balint Sept. 9, 2018, 7:47 p.m. UTC | #8
>>>> 2018.08.25. 20:35 keltezéssel, Marton Balint írta:
>>>>> To delay filtering until a given wallclock timestamp.
>>>>>
>>>>> Signed-off-by: Marton Balint <cus@passwd.hu>
>>>>> ---
>>>>>   doc/filters.texi         |  36 ++++++++++
>>>>>   libavfilter/Makefile     |   2 +
>>>>>   libavfilter/allfilters.c |   2 +
>>>>>   libavfilter/f_cue.c      | 182 
>>>>> +++++++++++++++++++++++++++++++++++++++++++++++
>>>>>   libavfilter/version.h    |   2 +-
>>>>>   5 files changed, 223 insertions(+), 1 deletion(-)
>>>>>   create mode 100644 libavfilter/f_cue.c
>>>>>

And finally applied. If Nicolas or anybody can provide some pointers on 
how to get rid of the extra frame queue, then I will be more than happy 
to work on it.

Thanks,
Marton
Paul B Mahol Sept. 9, 2018, 7:56 p.m. UTC | #9
On 9/9/18, Marton Balint <cus@passwd.hu> wrote:
>
>>>>> 2018.08.25. 20:35 keltezessel, Marton Balint irta:
>>>>>> To delay filtering until a given wallclock timestamp.
>>>>>>
>>>>>> Signed-off-by: Marton Balint <cus@passwd.hu>
>>>>>> ---
>>>>>>   doc/filters.texi         |  36 ++++++++++
>>>>>>   libavfilter/Makefile     |   2 +
>>>>>>   libavfilter/allfilters.c |   2 +
>>>>>>   libavfilter/f_cue.c      | 182
>>>>>> +++++++++++++++++++++++++++++++++++++++++++++++
>>>>>>   libavfilter/version.h    |   2 +-
>>>>>>   5 files changed, 223 insertions(+), 1 deletion(-)
>>>>>>   create mode 100644 libavfilter/f_cue.c
>>>>>>
>
> And finally applied. If Nicolas or anybody can provide some pointers on
> how to get rid of the extra frame queue, then I will be more than happy
> to work on it.

Perhaps you need #define FF_INTERNAL_FIELDS 1 ?
Nicolas George Sept. 9, 2018, 7:59 p.m. UTC | #10
Marton Balint (2018-09-09):
> And finally applied. If Nicolas or anybody can provide some pointers on how
> to get rid of the extra frame queue, then I will be more than happy to work
> on it.

I am sorry, I am currently having a very annoying trouble with my eye
which makes it easy to miss important mails and consumes a lot of time
and energy.

You should not have added a queue in your context at all, you should
have used the one already present using the API in filters.h.

Regards,
Marton Balint Sept. 9, 2018, 9:20 p.m. UTC | #11
On Sun, 9 Sep 2018, Nicolas George wrote:

> Marton Balint (2018-09-09):
>> And finally applied. If Nicolas or anybody can provide some pointers on how
>> to get rid of the extra frame queue, then I will be more than happy to work
>> on it.
>
> I am sorry, I am currently having a very annoying trouble with my eye
> which makes it easy to miss important mails and consumes a lot of time
> and energy.
>
> You should not have added a queue in your context at all, you should
> have used the one already present using the API in filters.h.

As far as I see there is only API to get the first frame in the fifo, and 
to check if there is at least one frame in the fifo. What I need is to get 
the frame->pts of the frames (first and last) in the fifo.

Paul suggested to #define FF_INTERNAL_FIELDS, this way I can access 
inlink->fifo directly, and I can use the framequeue.h functions on the 
inlink fifo. Is this what you are suggesting?

Thanks,
Marton
diff mbox

Patch

diff --git a/doc/filters.texi b/doc/filters.texi
index 32c95b591c..79eec0c808 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -551,6 +551,11 @@  Set LFO range.
 Set LFO rate.
 @end table
 
+@section acue
+
+Delay audio filtering until a given wallclock timestamp. See the @ref{cue}
+filter.
+
 @section adeclick
 Remove impulsive noise from input audio.
 
@@ -6987,6 +6992,37 @@  indicates 'never reset', and returns the largest area encountered during
 playback.
 @end table
 
+@anchor{cue}
+@section cue
+
+Delay video filtering until a given wallclock timestamp. The filter first
+passes on @option{preroll} amount of frames, then it buffers at most
+@option{buffer} amount of frames and waits for the cue. After reaching the cue
+it forwards the buffered frames and also any subsequent frames coming in its
+input.
+
+The filter can be used synchronize the output of multiple ffmpeg processes for
+realtime output devices like decklink. By putting the delay in the filtering
+chain and pre-buffering frames the process can pass on data to output almost
+immediately after the target wallclock timestamp is reached.
+
+Perfect frame accuracy cannot be guaranteed, but the result is good enough for
+some use cases.
+
+@table @option
+
+@item cue
+The cue timestamp expressed in a UNIX timestamp in microseconds. Default is 0.
+
+@item preroll
+The duration of content to pass on as preroll expressed in seconds. Default is 0.
+
+@item buffer
+The maximum duration of content to buffer before waiting for the cue expressed
+in seconds. Default is 0.
+
+@end table
+
 @anchor{curves}
 @section curves
 
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index e5d3a57af7..37a06e0ec0 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -36,6 +36,7 @@  OBJS-$(CONFIG_ACONTRAST_FILTER)              += af_acontrast.o
 OBJS-$(CONFIG_ACOPY_FILTER)                  += af_acopy.o
 OBJS-$(CONFIG_ACROSSFADE_FILTER)             += af_afade.o
 OBJS-$(CONFIG_ACRUSHER_FILTER)               += af_acrusher.o
+OBJS-$(CONFIG_ACUE_FILTER)                   += f_cue.o
 OBJS-$(CONFIG_ADECLICK_FILTER)               += af_adeclick.o
 OBJS-$(CONFIG_ADECLIP_FILTER)                += af_adeclick.o
 OBJS-$(CONFIG_ADELAY_FILTER)                 += af_adelay.o
@@ -178,6 +179,7 @@  OBJS-$(CONFIG_COREIMAGE_FILTER)              += vf_coreimage.o
 OBJS-$(CONFIG_COVER_RECT_FILTER)             += vf_cover_rect.o lavfutils.o
 OBJS-$(CONFIG_CROP_FILTER)                   += vf_crop.o
 OBJS-$(CONFIG_CROPDETECT_FILTER)             += vf_cropdetect.o
+OBJS-$(CONFIG_CUE_FILTER)                    += f_cue.o
 OBJS-$(CONFIG_CURVES_FILTER)                 += vf_curves.o
 OBJS-$(CONFIG_DATASCOPE_FILTER)              += vf_datascope.o
 OBJS-$(CONFIG_DCTDNOIZ_FILTER)               += vf_dctdnoiz.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 9732ae5345..6c6d0f43f0 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -27,6 +27,7 @@  extern AVFilter ff_af_abench;
 extern AVFilter ff_af_acompressor;
 extern AVFilter ff_af_acontrast;
 extern AVFilter ff_af_acopy;
+extern AVFilter ff_af_acue;
 extern AVFilter ff_af_acrossfade;
 extern AVFilter ff_af_acrusher;
 extern AVFilter ff_af_adeclick;
@@ -167,6 +168,7 @@  extern AVFilter ff_vf_coreimage;
 extern AVFilter ff_vf_cover_rect;
 extern AVFilter ff_vf_crop;
 extern AVFilter ff_vf_cropdetect;
+extern AVFilter ff_vf_cue;
 extern AVFilter ff_vf_curves;
 extern AVFilter ff_vf_datascope;
 extern AVFilter ff_vf_dctdnoiz;
diff --git a/libavfilter/f_cue.c b/libavfilter/f_cue.c
new file mode 100644
index 0000000000..732b5e218a
--- /dev/null
+++ b/libavfilter/f_cue.c
@@ -0,0 +1,182 @@ 
+/*
+ * Copyright (c) 2018 Marton Balint
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with FFmpeg; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavutil/opt.h"
+#include "libavutil/time.h"
+#include "avfilter.h"
+#include "filters.h"
+#include "framequeue.h"
+#include "internal.h"
+
+typedef struct CueContext {
+    const AVClass *class;
+    int64_t first_pts;
+    int64_t cue;
+    int64_t preroll;
+    int64_t buffer;
+    int status;
+    FFFrameQueue queue;
+} CueContext;
+
+static av_cold int init(AVFilterContext *ctx)
+{
+    CueContext *s = ctx->priv;
+    ff_framequeue_init(&s->queue, &ctx->graph->internal->frame_queues);
+    return 0;
+}
+
+static av_cold void uninit(AVFilterContext *ctx)
+{
+    CueContext *s = ctx->priv;
+    ff_framequeue_free(&s->queue);
+}
+
+static int activate(AVFilterContext *ctx)
+{
+    AVFilterLink *inlink = ctx->inputs[0];
+    AVFilterLink *outlink = ctx->outputs[0];
+    CueContext *s = ctx->priv;
+    int64_t pts;
+    AVFrame *frame = NULL;
+
+    FF_FILTER_FORWARD_STATUS_BACK(outlink, inlink);
+
+    if (s->status < 3 || s->status == 5) {
+        int ret = ff_inlink_consume_frame(inlink, &frame);
+        if (ret < 0)
+            return ret;
+        if (frame)
+            pts = av_rescale_q(frame->pts, inlink->time_base, AV_TIME_BASE_Q);
+    }
+
+    if (!s->status && frame) {
+        s->first_pts = pts;
+        s->status++;
+    }
+    if (s->status == 1 && frame) {
+        if (pts - s->first_pts < s->preroll)
+            return ff_filter_frame(outlink, frame);
+        s->first_pts = pts;
+        s->status++;
+    }
+    if (s->status == 2 && frame) {
+        int ret = ff_framequeue_add(&s->queue, frame);
+        if (ret < 0) {
+            av_frame_free(&frame);
+            return ret;
+        }
+        frame = NULL;
+        if (!(pts - s->first_pts < s->buffer && (av_gettime() - s->cue) < 0))
+            s->status++;
+    }
+    if (s->status == 3) {
+        int64_t diff;
+        while ((diff = (av_gettime() - s->cue)) < 0)
+            av_usleep(av_clip(-diff / 2, 100, 1000000));
+        s->status++;
+    }
+    if (s->status == 4) {
+        if (ff_framequeue_queued_frames(&s->queue))
+            return ff_filter_frame(outlink, ff_framequeue_take(&s->queue));
+        s->status++;
+    }
+    if (s->status == 5 && frame)
+        return ff_filter_frame(outlink, frame);
+
+    FF_FILTER_FORWARD_STATUS(inlink, outlink);
+    FF_FILTER_FORWARD_WANTED(outlink, inlink);
+
+    return FFERROR_NOT_READY;
+}
+
+#define OFFSET(x) offsetof(CueContext, x)
+#define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_AUDIO_PARAM | AV_OPT_FLAG_FILTERING_PARAM
+static const AVOption options[] = {
+    { "cue", "cue unix timestamp in microseconds", OFFSET(cue), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, FLAGS },
+    { "preroll", "preroll duration in seconds", OFFSET(preroll), AV_OPT_TYPE_DURATION, { .i64 = 0 }, 0, INT64_MAX, FLAGS },
+    { "buffer", "buffer duration in seconds", OFFSET(buffer), AV_OPT_TYPE_DURATION, { .i64 = 0 }, 0, INT64_MAX, FLAGS },
+    { NULL }
+};
+
+#if CONFIG_CUE_FILTER
+#define cue_options options
+AVFILTER_DEFINE_CLASS(cue);
+
+static const AVFilterPad cue_inputs[] = {
+    {
+        .name = "default",
+        .type = AVMEDIA_TYPE_VIDEO,
+    },
+    { NULL }
+};
+
+static const AVFilterPad cue_outputs[] = {
+    {
+        .name = "default",
+        .type = AVMEDIA_TYPE_VIDEO,
+    },
+    { NULL }
+};
+
+AVFilter ff_vf_cue = {
+    .name        = "cue",
+    .description = NULL_IF_CONFIG_SMALL("Delay filtering to match a cue."),
+    .priv_size   = sizeof(CueContext),
+    .priv_class  = &cue_class,
+    .init        = init,
+    .uninit      = uninit,
+    .inputs      = cue_inputs,
+    .outputs     = cue_outputs,
+    .activate    = activate,
+};
+#endif /* CONFIG_CUE_FILTER */
+
+#if CONFIG_ACUE_FILTER
+#define acue_options options
+AVFILTER_DEFINE_CLASS(acue);
+
+static const AVFilterPad acue_inputs[] = {
+    {
+        .name = "default",
+        .type = AVMEDIA_TYPE_AUDIO,
+    },
+    { NULL }
+};
+
+static const AVFilterPad acue_outputs[] = {
+    {
+        .name = "default",
+        .type = AVMEDIA_TYPE_AUDIO,
+    },
+    { NULL }
+};
+
+AVFilter ff_af_acue = {
+    .name        = "acue",
+    .description = NULL_IF_CONFIG_SMALL("Delay filtering to match a cue."),
+    .priv_size   = sizeof(CueContext),
+    .priv_class  = &acue_class,
+    .init        = init,
+    .uninit      = uninit,
+    .inputs      = acue_inputs,
+    .outputs     = acue_outputs,
+    .activate    = activate,
+};
+#endif /* CONFIG_ACUE_FILTER */
diff --git a/libavfilter/version.h b/libavfilter/version.h
index 0ac3a2f3a9..2ff2b6a318 100644
--- a/libavfilter/version.h
+++ b/libavfilter/version.h
@@ -30,7 +30,7 @@ 
 #include "libavutil/version.h"
 
 #define LIBAVFILTER_VERSION_MAJOR   7
-#define LIBAVFILTER_VERSION_MINOR  26
+#define LIBAVFILTER_VERSION_MINOR  27
 #define LIBAVFILTER_VERSION_MICRO 100
 
 #define LIBAVFILTER_VERSION_INT AV_VERSION_INT(LIBAVFILTER_VERSION_MAJOR, \