[FFmpeg-devel] [PATCH] lavfi: add asendcmd and sendcmd filters

Nicolas George nicolas.george at normalesup.org
Tue Aug 14 15:03:24 CEST 2012


L'octidi 28 thermidor, an CCXX, Stefano Sabatini a écrit :
> ---
>  doc/filters.texi         |   70 ++++++++++
>  libavfilter/Makefile     |    2 +
>  libavfilter/allfilters.c |    2 +
>  libavfilter/f_sendcmd.c  |  332 ++++++++++++++++++++++++++++++++++++++++++++++
>  4 files changed, 406 insertions(+), 0 deletions(-)
>  create mode 100644 libavfilter/f_sendcmd.c
> 
> diff --git a/doc/filters.texi b/doc/filters.texi
> index 763085c..ae5af7c 100644
> --- a/doc/filters.texi
> +++ b/doc/filters.texi
> @@ -4064,6 +4064,76 @@ tools.
>  
>  Below is a description of the currently available multimedia filters.
>  
> + at section asendcmd, sendcmd
> +
> +Send commands to filters in the filtergraph.
> +
> +These filters read commands to be sent to other filters in the
> +filtergraph.
> +

> + at code{asendcmd} must be inserted between two audio filters,
> + at code{sendcmd} must be inserted between two video filters, but apart
> +from that they act the same.

If I undestand correctly, the placement of the filters in the graph only
affect the time they see on the frames, right?

> +
> +The specification of commands can be specified in the filter arguments
> +with the @var{commands} option, or in a file specified with the
> + at var{filename} option.
> +

> +Commands are sent the first time when a frame with time greater or
> +equal to the specified command time is processed by the filter.

Hum. It works for encoding, but not for interactive playback where seeks
backwards happen. A lot of filters have that problem, though.

> +
> +These filters accept the following options:
> + at table @option
> + at item commands, c
> +Set the commands to be read and sent to the other filters.
> + at item filename, f
> +Set the filename of the commands to be read and sent to the other
> +filters.
> + at end table
> +
> + at subsection Commands syntax
> +
> +Commands are specified one per line. Empty lines or lines starting
> +with @code{#} are ignored.
> +
> +A command line has the syntax:
> + at example
> + at var{target} @var{time} @var{command} @var{arg}
> + at end example
> +
> + at var{target} specify the target of the command, usually the name of
> +the filter class or of the specific filter instance.
> +
> + at var{time} specify the time when the filter command is sent, expressed
> +as a time duration.
> +
> + at var{command} specify the name of the command, and depends on the
> +target filter.
> +
> + at var{arg} specify the optional list of argument for the given command.
> +
> + at subsection Examples
> +
> + at itemize
> + at item
> +Specify audio tempo change at second 4:
> + at example
> +asendcmd=c='atempo 4 tempo 1.5',atempo
> + at end example
> +
> + at item
> +Specify a list of drawtext commands in a file.
> + at example
> +drawtext 5 reinit fontsize=25:fontfile=FreeSerif.ttf:x=100+30*(t-5):text='hello world 5'
> +drawtext 10 reinit fontsize=25:fontfile=FreeSerif.ttf:x=100+30*(t-10):text='hello world 10'
> + at end example
> +
> +The file containing the list of commands can be specified with:
> + at example
> +sendcmd=f=test.cmd,drawtext=fontfile=FreeSerif.ttf:text='hello world 0'
> + at end example
> + at end itemize
> +
>  @section concat
>  
>  Concatenate audio and video streams, joining them together one after the
> diff --git a/libavfilter/Makefile b/libavfilter/Makefile
> index b6aeb76..422124c 100644
> --- a/libavfilter/Makefile
> +++ b/libavfilter/Makefile
> @@ -51,6 +51,7 @@ OBJS-$(CONFIG_AMERGE_FILTER)                 += af_amerge.o
>  OBJS-$(CONFIG_AMIX_FILTER)                   += af_amix.o
>  OBJS-$(CONFIG_ANULL_FILTER)                  += af_anull.o
>  OBJS-$(CONFIG_ARESAMPLE_FILTER)              += af_aresample.o
> +OBJS-$(CONFIG_ASENDCMD_FILTER)               += f_sendcmd.o
>  OBJS-$(CONFIG_ASETNSAMPLES_FILTER)           += af_asetnsamples.o
>  OBJS-$(CONFIG_ASETPTS_FILTER)                += vf_setpts.o
>  OBJS-$(CONFIG_ASETTB_FILTER)                 += f_settb.o
> @@ -115,6 +116,7 @@ OBJS-$(CONFIG_PIXDESCTEST_FILTER)            += vf_pixdesctest.o
>  OBJS-$(CONFIG_REMOVELOGO_FILTER)             += bbox.o lswsutils.o lavfutils.o vf_removelogo.o
>  OBJS-$(CONFIG_SCALE_FILTER)                  += vf_scale.o
>  OBJS-$(CONFIG_SELECT_FILTER)                 += vf_select.o
> +OBJS-$(CONFIG_SENDCMD_FILTER)                += f_sendcmd.o
>  OBJS-$(CONFIG_SETDAR_FILTER)                 += vf_aspect.o
>  OBJS-$(CONFIG_SETFIELD_FILTER)               += vf_setfield.o
>  OBJS-$(CONFIG_SETPTS_FILTER)                 += vf_setpts.o
> diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
> index 2949a4c..dd376d8 100644
> --- a/libavfilter/allfilters.c
> +++ b/libavfilter/allfilters.c
> @@ -41,6 +41,7 @@ void avfilter_register_all(void)
>      REGISTER_FILTER (AMIX,        amix,        af);
>      REGISTER_FILTER (ANULL,       anull,       af);
>      REGISTER_FILTER (ARESAMPLE,   aresample,   af);
> +    REGISTER_FILTER (ASENDCMD,    asendcmd,    af);
>      REGISTER_FILTER (ASETNSAMPLES, asetnsamples, af);
>      REGISTER_FILTER (ASETPTS,     asetpts,     af);
>      REGISTER_FILTER (ASETTB,      asettb,      af);
> @@ -105,6 +106,7 @@ void avfilter_register_all(void)
>      REGISTER_FILTER (REMOVELOGO,  removelogo,  vf);
>      REGISTER_FILTER (SCALE,       scale,       vf);
>      REGISTER_FILTER (SELECT,      select,      vf);
> +    REGISTER_FILTER (SENDCMD,     sendcmd,     vf);
>      REGISTER_FILTER (SETDAR,      setdar,      vf);
>      REGISTER_FILTER (SETFIELD,    setfield,    vf);
>      REGISTER_FILTER (SETPTS,      setpts,      vf);
> diff --git a/libavfilter/f_sendcmd.c b/libavfilter/f_sendcmd.c
> new file mode 100644
> index 0000000..46704ef
> --- /dev/null
> +++ b/libavfilter/f_sendcmd.c
> @@ -0,0 +1,332 @@
> +/*
> + * Copyright (c) 2012 Stefano Sabatini
> + *
> + * 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
> + */
> +
> +/**
> + * @file
> + * send commands filter
> + */
> +
> +#include "libavutil/avstring.h"
> +#include "libavutil/file.h"
> +#include "libavutil/opt.h"
> +#include "libavutil/parseutils.h"
> +#include "avfilter.h"
> +#include "internal.h"
> +#include "avfiltergraph.h"
> +#include "audio.h"
> +#include "video.h"
> +
> +typedef struct {
> +    char *buf;                  ///< allocated buffer
> +    char *target, *command, *arg;

> +    double t;

Any reason to use a double and not just an int64_t in AV_TIME_BASE or in
link->time_base? Floats are tricky.

> +} Command;
> +
> +static int cmp_commands(const void *a, const void *b)
> +{
> +    const Command *cmd1 = a;
> +    const Command *cmd2 = b;
> +
> +    return cmd1->t - cmd2->t;
> +}
> +
> +typedef struct {
> +    const AVClass *class;
> +    Command *commands;
> +    int   nb_commands;
> +    int next_command_idx;       ///< the index of the next command to process
> +
> +    char *commands_filename;
> +    char *commands_str;
> +
> +    uint8_t *commands_file_buf;
> +    size_t   commands_file_bufsize;
> +} SendCmdContext;
> +
> +#define OFFSET(x) offsetof(SendCmdContext, x)
> +
> +static const AVOption sendcmd_options[]= {
> +    { "commands", "set commands", OFFSET(commands_str), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0 },
> +    { "c",        "set commands", OFFSET(commands_str), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0 },
> +    { "filename", "set commands file",  OFFSET(commands_filename), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0 },
> +    { "f",        "set commands file",  OFFSET(commands_filename), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0 },
> +    {NULL},
> +};
> +
> +AVFILTER_DEFINE_CLASS(sendcmd);
> +

> +#define DELIMS " \f\t"

What happens if your lines are terminated by \r\n?

> +static int parse_command(Command *cmd, const char *cmd_string, void *log_ctx)
> +{
> +    char *timestr, *buf = strdup(cmd_string);
> +    int ret;
> +
> +    if (!buf)
> +        return AVERROR(ENOMEM);
> +
> +    /* format: target time command arg */
> +    cmd->buf    = buf;
> +    cmd->target = av_strtok(buf, DELIMS, &buf);
> +    timestr     = av_strtok(NULL, DELIMS, &buf);
> +    if (timestr) {
> +        int64_t us;
> +        if ((ret = av_parse_time(&us, timestr, 1)) < 0) {
> +            av_log(log_ctx, AV_LOG_ERROR, "Invalid time specification '%s'\n", timestr);
> +            return ret;
> +        }
> +        cmd->t = (double)us / 1000000;
> +    }
> +    cmd->command = av_strtok(NULL, DELIMS, &buf);
> +    cmd->arg     = buf;
> +
> +    if (!cmd->command) {
> +        av_log(log_ctx, AV_LOG_ERROR,
> +               "Parse error, command not specified in string '%s'\n", cmd_string);
> +        return AVERROR(EINVAL);
> +    }
> +
> +    return 0;
> +}
> +
> +static int parse_commands(Command **cmds, int *nb_cmds,
> +                          const char *cmds_buf, size_t cmds_bufsize,
> +                          void *log_ctx)
> +{
> +    char *line = NULL;
> +    size_t line_size = 0;
> +    int line_count;
> +    int ret, n = 0;
> +    const char *p = cmds_buf;
> +    const char *pend = cmds_buf + cmds_bufsize;
> +
> +    *nb_cmds = 0;
> +    if (!p)
> +        return 0;
> +    for (line_count = 0; *p; line_count++) {
> +        Command cmd;
> +        int i = 0;
> +

> +        /* skip empty lines && # comments */

I believe this comment goes a few lines later.

> +        while (p[i] != '\n' && p+i < pend)
> +            i++;
> +        line_size = i;

It makes multiline commands impossible, does it not?

> +
> +        if (*p == '\n' || *p == '#') {
> +            p += line_size+1;
> +            continue;
> +        }

What happens if a line starts with a space and then a comment?

> +
> +        line = av_realloc_f(line, line_size+1, 1);
> +        if (!line) {
> +            ret = AVERROR(ENOMEM);
> +            goto end;
> +        }
> +        memcpy(line, p, line_size);
> +        line[line_size] = 0;
> +
> +        if ((ret = parse_command(&cmd, line, log_ctx)) < 0) {
> +            av_log(log_ctx, AV_LOG_ERROR,
> +                   "Error occurred parsing line %d: %s\n", line_count, line);
> +            goto end;
> +        }
> +
> +        /* (re)allocate commands array if required*/
> +        if (*nb_cmds == n) {
> +            n = FFMAX(16, 2*n);
> +            *cmds = av_realloc_f(*cmds, n, 2*sizeof(Command));

> +            if (!*cmds) {
> +                ret = AVERROR(ENOMEM);
> +            }

It looks like there is a goto missing, or *cmds will be dereferenced anyway.

> +        }
> +        (*cmds)[(*nb_cmds)++] = cmd;
> +        p += line_size+1;
> +    }
> +
> +end:
> +    av_free(line);
> +    return ret;
> +}
> +
> +static av_cold int init(AVFilterContext *ctx, const char *args)
> +{
> +    SendCmdContext *sendcmd = ctx->priv;
> +    int ret, i;
> +    char *buf;
> +    size_t bufsize;
> +
> +    sendcmd->class = &sendcmd_class;
> +    av_opt_set_defaults(sendcmd);
> +
> +    if ((ret = av_set_options_string(sendcmd, args, "=", ":")) < 0)
> +        return ret;
> +
> +    if (sendcmd->commands_filename && sendcmd->commands_str) {
> +        av_log(ctx, AV_LOG_ERROR,
> +               "Only one of the filename or commands options must be specified\n");
> +        return AVERROR(EINVAL);
> +    }
> +
> +    if (sendcmd->commands_filename) {
> +        ret = av_file_map(sendcmd->commands_filename,
> +                          &sendcmd->commands_file_buf,
> +                          &sendcmd->commands_file_bufsize, 0, ctx);
> +        if (ret < 0)
> +            return ret;
> +        buf     = sendcmd->commands_file_buf;
> +        bufsize = sendcmd->commands_file_bufsize;
> +    } else {
> +        buf     = sendcmd->commands_str;
> +        bufsize = strlen(sendcmd->commands_str) + 1;
> +    }
> +
> +    if ((ret = parse_commands(&sendcmd->commands, &sendcmd->nb_commands,
> +                              buf, bufsize, ctx)) < 0)
> +        return ret;
> +

> +    qsort(sendcmd->commands, sendcmd->nb_commands, sizeof(Command), cmp_commands);

The user can specify several commands for the same time, and the order may
be relevant: a stable sort would be required.

> +
> +    av_log(ctx, AV_LOG_DEBUG, "Parsed commands:\n");
> +    for (i = 0; i < sendcmd->nb_commands; i++) {
> +        Command cmd = sendcmd->commands[i];
> +        av_log(ctx, AV_LOG_DEBUG, "target:%s time:%f command:%s arg:%s\n",
> +               cmd.target, cmd.t, cmd.command, cmd.arg);
> +    }
> +
> +    return 0;
> +}
> +
> +static void av_cold uninit(AVFilterContext *ctx)
> +{
> +    SendCmdContext *sendcmd = ctx->priv;
> +    int i;
> +
> +    av_opt_free(sendcmd);
> +    for (i = 0; i < sendcmd->nb_commands; i++)
> +        av_free(sendcmd->commands[i].buf);
> +
> +    av_freep(&sendcmd->commands);
> +    av_file_unmap(sendcmd->commands_file_buf,
> +                  sendcmd->commands_file_bufsize);
> +}
> +
> +static int process_frame(AVFilterLink *inlink, AVFilterBufferRef *ref)
> +{
> +    AVFilterContext *ctx = inlink->dst;
> +    SendCmdContext *sendcmd = ctx->priv;
> +    double t;
> +    int ret;
> +
> +    if (ref->pts == AV_NOPTS_VALUE)
> +        goto end;
> +
> +    t = ref->pts * av_q2d(inlink->time_base);
> +
> +    while (sendcmd->next_command_idx < sendcmd->nb_commands &&
> +           t >= (sendcmd->commands[sendcmd->next_command_idx]).t) {
> +        char buf[1024];
> +        Command *cmd = &sendcmd->commands[sendcmd->next_command_idx];
> +
> +        av_log(ctx, AV_LOG_VERBOSE,
> +               "Processing command target:%s time:%f command:%s arg:%s\n",
> +               cmd->target, cmd->t, cmd->command, cmd->arg);
> +
> +        ret = avfilter_graph_send_command(inlink->graph,
> +                                          cmd->target, cmd->command, cmd->arg,
> +                                          buf, sizeof(buf),
> +                                          AVFILTER_CMD_FLAG_ONE);
> +        av_log(ctx, AV_LOG_VERBOSE,
> +               "Command reply for command '%s': ret:%s res:%s\n", cmd->buf, av_err2str(ret), buf);
> +        sendcmd->next_command_idx++;
> +    }
> +
> +end:
> +    switch (inlink->type) {
> +    case AVMEDIA_TYPE_VIDEO: return ff_start_frame   (inlink->dst->outputs[0], ref);
> +    case AVMEDIA_TYPE_AUDIO: return ff_filter_samples(inlink->dst->outputs[0], ref);
> +    }
> +    return AVERROR(ENOSYS);
> +}
> +

> +static int end_frame(AVFilterLink *inlink)
> +{
> +    inlink->cur_buf = NULL;
> +    return ff_end_frame(inlink->dst->outputs[0]);
> +}

Any reason not to clear cur_buf in process_frame?

> +
> +#if CONFIG_SENDCMD_FILTER
> +
> +AVFilter avfilter_vf_sendcmd = {
> +    .name      = "sendcmd",
> +    .description = NULL_IF_CONFIG_SMALL("Send commands to filters."),
> +
> +    .init = init,
> +    .uninit = uninit,
> +    .priv_size = sizeof(SendCmdContext),
> +
> +    .inputs = (const AVFilterPad[]) {
> +        {
> +            .name             = "default",
> +            .type             = AVMEDIA_TYPE_VIDEO,
> +            .get_video_buffer = ff_null_get_video_buffer,
> +            .start_frame      = process_frame,
> +            .end_frame        = end_frame
> +        },
> +        { .name = NULL }
> +    },
> +    .outputs = (const AVFilterPad[]) {
> +        {
> +            .name             = "default",
> +            .type             = AVMEDIA_TYPE_VIDEO,
> +        },
> +        { .name = NULL }
> +    },
> +};
> +
> +#endif
> +
> +#if CONFIG_ASENDCMD_FILTER
> +
> +AVFilter avfilter_af_asendcmd = {
> +    .name      = "asendcmd",
> +    .description = NULL_IF_CONFIG_SMALL("Send commands to filters."),
> +
> +    .init = init,
> +    .uninit = uninit,
> +    .priv_size = sizeof(SendCmdContext),
> +
> +    .inputs = (const AVFilterPad[]) {
> +        {
> +            .name             = "default",
> +            .type             = AVMEDIA_TYPE_AUDIO,
> +            .get_audio_buffer = ff_null_get_audio_buffer,
> +            .filter_samples   = process_frame,
> +        },
> +        { .name = NULL }
> +    },
> +    .outputs = (const AVFilterPad[]) {
> +        {
> +            .name             = "default",
> +            .type             = AVMEDIA_TYPE_AUDIO,
> +        },
> +        { .name = NULL }
> +    },
> +};
> +
> +#endif

That is an useful feature, thanks.

Regards,

-- 
  Nicolas George
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 198 bytes
Desc: Digital signature
URL: <http://ffmpeg.org/pipermail/ffmpeg-devel/attachments/20120814/d47c2821/attachment.asc>


More information about the ffmpeg-devel mailing list